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