Files
2026-10-07 13:06:47 +05:00

7.0 KiB
Raw Permalink Blame History

Лабораторная работа 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:

  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. Увеличьте размер задачи.