7.0 KiB
Лабораторная работа 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 воркеров-процессов:
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:
run_cpu_processes(task_name, params, p)— CPU-задача наpпроцессах черезPoolиdispatch_kernel. Вернуть(checksum, сек). Вся логика — подif __name__ == "__main__"(вmain()).measure_overhead(items_count)— измерить стоимость передачи: отправить в воркер список изitems_countчисел, воркер возвращает его длину. Время / количество = цена байта. Вернуть{items_count: сек}.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.
Контрольные вопросы
- Почему процессы ускоряют CPU-код, а потоки — нет?
- Что такое pickle и почему лямбда не может быть аргументом
pool.map? - Замерили T(p=8) на 4-ядерной машине с гиперторингом — ускорение ~4, а не 8. Почему?
- Когда передача данных между процессами дороже самих вычислений?
- Почему на 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. Увеличьте размер задачи.