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

121 lines
7.2 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.
# Лабораторная работа 2. Многопоточный код (I/O-bound)
**Время на работу:** 2 пары. **Зависимости:** лаба 1.
## Цель работы
Освоить потоки на задаче, где они реально дают ускорение — I/O-bound.
К концу работы вы должны уметь объяснить, почему один и тот же механизм
бесполезен для CPU-задач (лаба 3).
## Теория
### Что такое поток
Поток (thread) — независимая последовательность инструкций **внутри одного
процесса**. Все потоки делят общую память: один и тот же список, словарь,
глобальную переменную видно из любого потока. Это делает потоки лёгкими
(создание ~десятки микросекунд, у процессов — миллисекунды) и опасными
одновременно: два потока могут менять одну переменную без ведома друг друга
(лаба 5).
### Почему потоки ускоряют I/O-bound, но не CPU-bound
Когда поток выполняет `time.sleep`, чтение сокета или диска, он **отпускает
процессор** — ОС переключает его на другой поток. Десять потоков, ждущих сеть,
«спят» одновременно, и суммарное время ≈ времени одного запроса.
Когда поток считает (`x = a * b`), он держит процессор, а в CPython — ещё и
**GIL** (Global Interpreter Lock): глобальный замок, разрешающий исполнять
байт-код Python только одному потоку процесса. Поэтому CPU-код потоками не
ускоряется в принципе (подробно — лаба 3).
Правило: **потоки — для ожидания, процессы — для вычислений.**
### threading vs concurrent.futures
`threading.Thread` — ручное управление: создать, `start()`, `join()`,
самому разложить данные и собрать результаты. Полезно понять один раз.
`concurrent.futures.ThreadPoolExecutor` — пул потоков и высокоуровневый
интерфейс:
```python
with ThreadPoolExecutor(max_workers=p) as pool:
results = list(pool.map(io_fetch, items)) # порядок входа сохранён
futures = [pool.submit(io_fetch, i) for i in items] # или Future-объекты
```
`submit` возвращает `Future` — «обещание» результата: `.result()` блокирует
до готовности, `.done()` проверяет готовность, можно навесить колбэк
`add_done_callback`. `pool.map` — как встроенный `map`, но параллельный;
результаты приходят в порядке входа.
В работе используются оба способа: `Thread` — чтобы понять механику,
`ThreadPoolExecutor` — как рабочий инструмент.
## Задание
I/O-нагрузка та же, что в лабе 1 (`io_fetch` из `common.tasks`: имитация
запроса задержкой 0.05 c). Параметры items — из вашего варианта.
Заполните `solution.py`:
1. **`run_io_threads_manual(items, delay, p)`** — разбить `range(items)` на
`p` кусков, на каждый кусок — свой `threading.Thread`. Каждый поток
кладёт результаты в **свой** список (общая структура без синхронизации —
пока избегаем). Собрать куски по порядку.
2. **`run_io_threads_pool(items, delay, p)`** — то же через
`ThreadPoolExecutor(max_workers=p)` и `pool.map`.
3. **`main()`** — замерить T(1), T(2), T(4) (и T(8), если есть в варианте)
обоими способами; построить таблицу и график ускорения
(`common.benchmark`).
### Ожидаемые результаты (проверьте себя)
- T(1) ≈ items × delay (например 24 × 0.05 = 1.2 c);
- T(4) ≈ items × delay / 4 (если items кратно 4) — почти идеальное ускорение;
- ручные потоки и пул дают примерно одинаковое время.
Если T(4) ≈ T(1) — вы где-то синхронизируете лишний раз или запускаете
потоки по очереди (проверьте: `start()` всех потоков должен быть **до**
первого `join()`).
### Дополнительно (по желанию, +1 балл)
- `ThreadPoolExecutor` + `submit`/`as_completed`: выводить номер завершившегося
запроса по мере готовности — увидите, что порядок завершения не совпадает
с порядком запуска.
## Контрольные вопросы
1. Почему `time.sleep` в 4 потоках занимает те же 0.05 c, а не 0.2 c?
2. Чем `Future` отличается от готового результата? Что делает `.result()`?
3. Почему потоки имеют общую память и чем это опасно?
4. Что произойдёт с вашей I/O-задачей, если `max_workers=100`? Почему рост
останавливается?
## Что сдаётся
`solution.py` + отчёт: таблица T(p) обоих способов, график S(p), анализ
(совпало ли с ожидаемым, где видны накладные расходы на создание потоков),
ответы на контрольные вопросы.
## Критерии оценки
| Пункт | Баллы |
|---|---|
| Ручные потоки: результаты корректны и упорядочены | 2 |
| Пул потоков: результаты корректны | 2 |
| Замеры T(1/2/4), ускорение ≈ p на I/O-части | 2 |
| График и анализ в отчёте | 2 |
| Ответы на контрольные вопросы | 2 |
## Типичные ошибки
- `start()` и сразу `join()` в одном цикле — потоки выполнятся по очереди.
- Общий список без блокировки (`results.append(...)` из многих потоков) —
в этой лабе «повезёт» (append атомарен из-за GIL), но полагаться на это
нельзя — в лабе 5 это сломают демонстративно.
- Забыли `args=(chunk,)` в `Thread` — функция вызвалась в главном потоке.