Files
parallel-programming/lab05_sync/методичка.md
T
2026-10-07 13:06:47 +05:00

115 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Лабораторная работа 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` в другом порядке → дедлок
(порядок взятия замков должен быть единым).