# Лабораторная работа 5. Синхронизация и гонки данных **Время на работу:** 2 пары. **Зависимости:** лабы 2, 4. ## Цель работы Увидеть гонку данных своими глазами (без синхронизации результат «плавает»), научиться защищать общие ресурсы (`Lock`, `Semaphore`, `Event`) и собрать классическую схему producer–consumer без дедлоков. ## Теория ### Гонка данных Гонка — ситуация, когда результат зависит от порядка доступа потоков к общим данным. Классика: `counter += 1` — это **три** операции (прочитать, увеличить, записать). Два потока могут прочитать одно значение, оба увеличить, оба записать — одно из двух увеличений потеряется. Важно: `x += 1` в Python **не атомарен**. Атомарны отдельные операции вроде `list.append` (спорно, но под GIL обычно надёжно) — полагаться на это нельзя. ### Инструменты синхронизации | Инструмент | Что делает | Когда нужен | |---|---|---| | `Lock` | взаимное исключение: только один в критической секции | защита счётчиков, списков | | `RLock` | Lock, который можно взять повторно тем же потоком | рекурсивные функции | | `Semaphore(n)` | разрешает не более n одновременных входов | лимит на соединения/ресурс | | `Event` | флаг «событие произошло»: `set()` / `wait()` | сигнал между потоками | | `Condition` | ожидание условия (`wait`/`notify`) | сложные очереди | | `Barrier(n)` | все ждут, пока соберутся n | синхронный старт | В multiprocessing те же примитивы есть в `multiprocessing` (они работают между процессами через разделяемую память ОС). ### Producer–consumer Классическая схема: производители кладут задачи в очередь, потребители забирают. `queue.Queue` — потокобезопасна сама по себе (внутри Lock + Condition). Главные грабли: - **Дедлок**: потребитель ждёт `get()`, а производитель уже закончил. Решение: «маркер конца» (`put(None)`) — по одному на каждого потребителя, или `get(timeout=...)`/`get_nowait()`. - **Забытый join** очереди или потоков — программа завершается раньше времени. ### Как искать гонки - Замеры не помогут — гонка «мимикрирует». Помогает: 1. увеличение числа потоков и повторов; 2. `time.sleep(0)` внутри критической секции — усиливает переключения; 3. сравнение результата с эталоном (checksum). ## Задание Заполните `solution.py`: 1. **`race_demo(n_threads, n_increments)`** — n потоков увеличивают общий счётчик `n_increments` раз каждый БЕЗ синхронизации. Вернуть `(фактический_счётчик, ожидаемый)`. Фактический должен быть МЕНЬШЕ ожидаемого хотя бы иногда — если всегда ровно, увеличьте параметры и повторите (см. «Как искать гонки»). 2. **`race_fixed(n_threads, n_increments)`** — то же с `threading.Lock`. Результат обязан быть точным всегда. Замерьте, во что обходится Lock. 3. **`semaphore_limiter(items, delay, max_concurrent)`** — выполнить `items` «запросов» `io_fetch` на потоках, но не более `max_concurrent` одновременно (`Semaphore` + ThreadPoolExecutor). Вернуть `(результаты, сек)`. 4. **`producer_consumer(n_producers, n_consumers, n_items)`** — производители кладут `n_items` задач суммарно, потребители обрабатывают, программа завершается без дедлока. Вернуть количество обработанных задач. 5. **`main()`** — демонстрация всех пункто�� с замерами. ### Ожидаемые результаты - `race_demo`: счётчик < ожидаемого (часто сильно); при больших `n_increments` потери растут. - `race_fixed`: всегда ровно; время чуть больше (цена Lock). - `semaphore_limiter`: время ≈ `items / max_concurrent × delay` — лимит работает. - `producer_consumer`: обработано ровно `n_items`, завершается сам. ## Контрольные вопросы 1. Почему `counter += 1` не атомарен? Какие операции он включает? 2. Чем `Semaphore(3)` отличается от трёх `Lock`? Где это полезно? 3. Что такое дедлок и как «маркер конца» его предотвращает? 4. Почему `queue.Queue` не требует внешнего Lock? 5. Приведите пример из вашей CPU-задачи, где нужна синхронизация при многопоточности (например, сборка результатов). ## Что сдаётся `solution.py` + отчёт: вывод race_demo (несколько запусков!), сравнение race_fixed, замеры semaphore_limiter, описание схемы producer–consumer, ответы на вопросы. ## Критерии оценки | Пункт | Баллы | |---|---| | race_demo демонстрирует потери (или обоснование, почему не демонстрирует) | 2 | | race_fixed даёт точный результат всегда | 2 | | semaphore_limiter: лимит соблюдается, замеры есть | 2 | | producer–consumer завершается сам, ничего не теряется | 2 | | Отчёт и ответы на вопросы | 2 | ## Типичные ошибки - race_demo всегда даёт точный результат → увеличьте n_increments (10⁵+), число потоков, добавьте `time.sleep(0)` после чтения счётчика. - producer–consumer висит → забыли маркеры конца или не join() потоки. - `Lock` взят внутри `with` другого `Lock` в другом порядке → дедлок (порядок взятия замков должен быть единым).