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

7.5 KiB
Raw Permalink Blame History

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