Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,119 @@
|
||||
# Лабораторная работа 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) «плавает».
|
||||
Reference in new issue
Block a user