This commit is contained in:
lovinervy committed 2026-10-07 13:06:47 +05:00
commit c394677d07
59 files changed
+5149

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
# Лабораторная работа 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` в другом порядке → дедлок
(порядок взятия замков должен быть единым).