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

119 lines
7.0 KiB
Markdown
Raw Permalink 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.
# Лабораторная работа 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. Увеличьте размер задачи.