# Лабораторная работа 4. Мультипроцессорный код **Время на работу:** 2 пары. **Зависимости:** лабы 1–3. ## Цель работы Освоить настоящую параллельность на CPU-задачах: процессы обходят GIL. Измерить накладные расходы multiprocessing и понять, когда они съедают выигрыш. ## Теория ### Процесс против потока Процесс — отдельный экземпляр интерпретатора со **своей** памятью. У каждого процесса свой GIL, поэтому CPU-задачи на p процессах исполняются по-настоящему параллельно (на p ядрах). Цена: создание процесса ~10–100 мс (против ~0.1 мс у потока), а любые данные передаются между процессами через **pickle** — сериализацию. Большие данные (матрицы, списки миллионов чисел) передаются долго, и это становится главным ограничением. ### Методы запуска: fork и spawn - **fork** (Linux/macOS): дочерний процесс — копия родителя «снимком» памяти. Быстро, но опасно с потоками и блокировками. - **spawn** (Windows, по умолчанию на macOS): запускается новый интерпретатор, который заново импортирует ваш модуль. Медленнее и строже: **весь код создания процессов должен быть под `if __name__ == "__main__":`**, иначе бесконечное размножение процессов. Проверить и выбрать: `multiprocessing.get_start_method()` / `multiprocessing.get_context("spawn")`. ### Pool и передаваемые функции `multiprocessing.Pool(p)` — пул из p воркеров-процессов: ```python with multiprocessing.Pool(p) as pool: results = pool.map(kernel, chunks) # как map, но по процессам ``` ВАЖНО: `kernel` и `chunks` должны пиккелиться. Вложенные функции (def внутри def) и лямбды — **не** пиккелятся. Поэтому в `common.tasks` ядро задачи вызывается через модульный `dispatch_kernel((name, chunk))`. ### Что именно передаётся Схема split → kernel → combine в мире процессов означает: - `chunks` сериализуются и уходят воркерам (расход на передачу); - частичные результаты сериализуются и возвращаются (расход на возврат); - если данные больше результата — передача может занять дольше, чем счёт (см. задачу «сортировка» в банке задач). ### Разные способы организации - `Pool.map` — распределить список по воркерам; - `Process` + `Queue` — ручная схема: воркеры берут задачи из очереди и кладут ответы в другую (задел для лабы 5); - `ProcessPoolExecutor` —ThreadPoolExecutor-подобный интерфейс для процессов. ## Задание CPU-задача — из вашего варианта. Заполните `solution.py`: 1. **`run_cpu_processes(task_name, params, p)`** — CPU-задача на `p` процессах через `Pool` и `dispatch_kernel`. Вернуть `(checksum, сек)`. Вся логика — под `if __name__ == "__main__"` (в `main()`). 2. **`measure_overhead(items_count)`** — измерить стоимость передачи: отправить в воркер список из `items_count` чисел, воркер возвращает его длину. Время / количество = цена байта. Вернуть `{items_count: сек}`. 3. **`main()`** — T(p) для p из варианта; таблица; график S(p) с «идеальной» прямой; вывод о накладных расходах. ### Ожидаемые результаты - CPU-задача: S(p) растёт почти линейно до числа физических ядер; на машинах с гиперторингом S(p_max) может упираться в ~число физ. ядер. - `measure_overhead`: передача маленьких данных ~миллисекунды, больших — десятки-сотни мс. Сравните с временем kernel вашего варианта. ### Важно (Windows) - Всё создание пулов — строго внутри `main()`/под `__main__`. - Не используйте лямбды и вложенные функции как аргументы `pool.map`. - На macOS/Linux можно сравнить `fork` и `spawn` (`get_context`) — добавьте в отчёт, если работаете не на Windows. ## Контрольные вопросы 1. Почему процессы ускоряют CPU-код, а потоки — нет? 2. Что такое pickle и почему лямбда не может быть аргументом `pool.map`? 3. Замерили T(p=8) на 4-ядерной машине с гиперторингом — ускорение ~4, а не 8. Почему? 4. Когда передача данных между процессами дороже самих вычислений? 5. Почему на Windows нужен `if __name__ == "__main__":`? ## Что сдаётся `solution.py` + отчёт: таблица T(p), график S(p), замеры overhead передачи, анализ (до какого p растёт, где упирается), ответы на вопросы. ## Критерии оценки | Пункт | Баллы | |---|---| | Корректный запуск на процессах, checksum совпадает | 2 | | S(p) > 1.5 при p=4 на CPU-задаче | 2 | | Замер overhead передачи данных | 2 | | График + анализ накладных расходов | 2 | | Ответы на контрольные вопросы | 2 | ## Типичные ошибки - Нет `__main__` — на Windows бесконечный spawn (см. типичные_ошибки п.1). - Лямбда/вложенная функция в `pool.map` — `PicklingError`. - Слишком мелкие куски: создание процессов и передача дороже счёта, S(p) < 1. Увеличьте размер задачи.