Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,118 @@
|
||||
# Лабораторная работа 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. Увеличьте размер задачи.
|
||||
Reference in new issue
Block a user