7.4 KiB
Лабораторная работа 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 очереди или потоков — программа завершается раньше времени.
Как искать гонки
- Замеры не помогут — гонка «мимикрирует». Помогает:
- увеличение числа потоков и повторов;
time.sleep(0)внутри критической секции — усиливает переключения;- сравнение результата с эталоном (checksum).
Задание
Заполните solution.py:
race_demo(n_threads, n_increments)— n потоков увеличивают общий счётчикn_incrementsраз каждый БЕЗ синхронизации. Вернуть(фактический_счётчик, ожидаемый). Фактический должен быть МЕНЬШЕ ожидаемого хотя бы иногда — если всегда ровно, увеличьте параметры и повторите (см. «Как искать гонки»).race_fixed(n_threads, n_increments)— то же сthreading.Lock. Результат обязан быть точным всегда. Замерьте, во что обходится Lock.semaphore_limiter(items, delay, max_concurrent)— выполнитьitems«запросов»io_fetchна потоках, но не болееmax_concurrentодновременно (Semaphore+ ThreadPoolExecutor). Вернуть(результаты, сек).producer_consumer(n_producers, n_consumers, n_items)— производители кладутn_itemsзадач суммарно, потребители обрабатывают, программа завершается без дедлока. Вернуть количество обработанных задач.main()— демонстрация всех пункто�� с замерами.
Ожидаемые результаты
race_demo: счётчик < ожидаемого (часто сильно); при большихn_incrementsпотери растут.race_fixed: всегда ровно; время чуть больше (цена Lock).semaphore_limiter: время ≈items / max_concurrent × delay— лимит работает.producer_consumer: обработано ровноn_items, завершается сам.
Контрольные вопросы
- Почему
counter += 1не атомарен? Какие операции он включает? - Чем
Semaphore(3)отличается от трёхLock? Где это полезно? - Что такое дедлок и как «маркер конца» его предотвращает?
- Почему
queue.Queueне требует внешнего Lock? - Приведите пример из вашей 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в другом порядке → дедлок (порядок взятия замков должен быть единым).