Files
2026-10-07 13:06:47 +05:00

7.4 KiB
Raw Permalink Blame History

Лабораторная работа 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 в другом порядке → дедлок (порядок взятия замков должен быть единым).