Files
parallel-programming/lab03_gil/методичка.md
T
2026-10-07 13:06:47 +05:00

120 lines
7.5 KiB
Markdown
Raw 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.
# Лабораторная работа 3. GIL: почему потоки не ускоряют CPU-код
**Время на работу:** 2 пары. **Зависимости:** лабы 1–2.
## Цель работы
Экспериментально исследовать GIL (Global Interpreter Lock) и научиться
обходить его тремя способами: процессы, освобождающие GIL C-функции
(`hashlib`), мультипоточность вне GIL (превью `sys._is_gil_enabled`).
## Теория
### Что такое GIL
GIL — мьютекс в CPython, разрешающий исполнять байт-код Python только одному
потоку процесса в каждый момент. Один интерпретатор — один «держатель» GIL.
Зачем он нужен: упрощает реализацию CPython (счётчики ссылок, внутренние
структуры не защищены отдельными замками) и делает однопоточный код быстрым.
Потоки в CPython — **настоящие** потоки ОС, но байт-код Python они исполняют
по очереди.
Как поток получает процессор: держатель GIL обязан отпускать его
периодически (по умолчанию каждые ~5 мс, `sys.getswitchinterval()`), и при
блокирующих вызовах (`sleep`, чтение сети/диска).
### Следствия
| Тип задачи | Потоки | Почему |
|---|---|---|
| I/O-bound | ускоряют почти идеально | GIL отпускается на время ожидания |
| CPU-bound (чистый Python) | НЕ ускоряют | GIL: байт-код исполняется по очереди |
| CPU-bound (C-код внутри) | МОГУТ ускорять | C-функция может отпустить GIL |
Третий пункт — самый интересный: библиотеки на C (`hashlib`, `zlib`,
`numpy` во многих операциях) во время длинных вычислений **отпускают GIL**,
и потоки работают по-настоящему параллельно. Тонкость: GIL отпускается на
время **одного вызова** C-функции. Если блоки маленькие (4 КБ), вызовы
слишком короткие и GIL тут же забирается обратно — ускорения не будет.
Блоки ≥ 256 КБ (в работе — 1 МБ) дают честный параллелизм. В лабе это
демонстрирует `hashlib`.
### Свободнный threading (Python 3.13+, превью)
Начиная с CPython 3.13 существует экспериментальная сборка без GIL
(free-threaded). В обычном CPython можно проверить, включён ли GIL:
`sys._is_gil_enabled()`. В работе — короткое ознакомление, не основное.
### Проверка GIL в эксперименте
Метод: фиксированная CPU-работа `W`. Замеряем T(1), затем ту же работу
на p потоках. Ускорение S = T(1)/T(p):
- S ≈ 1 → GIL не отпускался (чистый Python);
- S ≈ p → GIL отпускался (C-код, sleep).
## Задание
CPU-задача — из вашего варианта (`common.tasks`), ядро `kernel` работает на
чистом Python. Заполните `solution.py`:
1. **`measure_cpu_threads(task_name, params, p_list)`** — для каждого p:
последовательный запуск (p=1, база) и запуск CPU-задачи на p **потоках**
(`ThreadPoolExecutor`, каждый поток — свой кусок). Вернуть
`{p: время}`. Убедиться, что checksum совпадает с последовательным.
2. **`measure_gil_release(p_list, block_size, blocks_per_part)`** — то же,
но работа — SHA-256 над готовыми блоками байтов (освобождает GIL).
Фиксированный общий объём `W = blocks_per_part * max(p_list)` блоков по
`block_size` байт (`os.urandom`), делится между p потоками. Вернуть
`{p: время}`, проверять checksum.
3. **`gil_status()`** — вернуть `(python_version, gil_enabled)` через
`sys._is_gil_enabled()` (если атрибута нет — GIL включён).
4. **`main()`** — таблицы ускорений для обоих случаев + график S(p).
### Ожидаемые результаты
- Чистый Python: S(4) ≈ 0.9–1.1 (замедление из-за переключений GIL).
- SHA-256 на блоках 1 МБ: S(4) ≈ 2–4 (реальный параллелизм).
- SHA-256 на блоках 4 КБ: S(4) ≈ 0.8 — GIL не успевает отпускаться.
**Попробуйте оба размера блоков и объясните разницу** — это ядро работы.
- Разница между «чистый Python» и «hashlib 1 МБ» — и есть GIL.
### Важно про честность замеров
- объём работы при любом p одинаковый (проверка checksum);
- `time_call(..., repeats=3)`, берём лучшее;
- следите, чтобы данные hashing-задачи были крупными блоками (в банке
задач так и сделано).
## Контрольные вопросы
1. Что такое GIL и зачем он в CPython?
2. Почему `time.sleep` в 100 потоках не «съедает» процессор, а 100
считающих потоков работают как один?
3. Почему `hashlib.sha256` ускоряется потоками, а `sum` по списку — нет?
4. Что изменится в Python 3.13+ free-threaded сборках? Найдите статус PEP 703.
5. Как проверить в вашем эксперименте, что работа распределена честно?
## Что сдаётся
`solution.py` + отчёт: таблица S(p) для чистого Python и для hashlib,
график с обеими кривыми и «идеальной» прямой, вывод: когда потоки полезны.
Ответы на контрольные вопросы.
## Критерии оценки
| Пункт | Баллы |
|---|---|
| Корректный замер чистого Python (S ≈ 1) | 2 |
| Корректный замер hashlib (S(p) > 1.5 при p=4) | 2 |
| `gil_status` и версия Python в отчёте | 1 |
| График с тремя кривыми | 2 |
| Анализ и ответы на вопросы | 3 |
## Типичные ошибки
- Слишком мелкие блоки для hashlib (4 КБ): S ≈ 0.8, а не 3+. GIL отпускается
на время одного вызова — вызовы должны быть долгими (блоки ≥ 256 КБ).
- Разный объём работы при разных p — обязательно проверяйте checksum.
- Замер на ноутбуке без питания — турбо-частоты скачут, S(p) «плавает».