# Лабораторная работа 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) «плавает».