# Лабораторная работа 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` — функция вызвалась в главном потоке.