PythonMiddleTechnical
В каких случаях GIL почти не мешает?
GIL почти не мешает: при однопоточном коде, I/O-bound нагрузке (потоки ждут на сети/диске/БД с release GIL), при работе через multiprocessing (отдельный GIL на процесс), при использовании C-extensions с PY_BEGIN_ALLOW_THREADS (NumPy, lxml, hashlib), и на free-threaded build (3.13+).
Когда GIL не bottleneck
- Однопоточная программа. GIL вообще не вступает в игру — конкурентов нет.
- I/O-bound multi-threading. Потоки большую часть времени ждут на recv/read/select — на blocking syscall GIL отпускается; запросы реально распараллеливаются.
- asyncio. Один поток с event loop; GIL не влияет на пропускную способность async I/O.
- multiprocessing. Каждый процесс — свой интерпретатор и свой GIL; CPU-bound масштабируется по ядрам.
- C-extensions с release GIL. NumPy, SciPy, pandas (частично), lxml, hashlib, zlib, ssl, sqlite3, Pillow, cryptography, OpenCV — внутри тяжёлых операций делают
Py_BEGIN_ALLOW_THREADS, что освобождает GIL до окончания работы. - subinterpreters (PEP 684, 3.12+) и free-threaded build (PEP 703, 3.13+ experimental) снимают/ослабляют ограничение GIL.
- JIT runtimes: PyPy не имеет GIL? — на самом деле тоже имеет, но с stm/perf-оптимизациями. Полностью без GIL — IronPython, Jython, GraalPy.
Когда GIL «незаметен» из-за метрик
- Bottleneck — БД/внешний API. Время в Python ничтожно по сравнению с round-trip.
- Системные вызовы доминируют (logging на медленный диск, парсинг через системный JSON).
- Используется gunicorn/uvicorn с несколькими воркерами — на уровне сервиса масштабирование уже идёт через процессы.
Пример
import hashlib
import time
from concurrent.futures import ThreadPoolExecutor
# I/O-bound: requests делает blocking recv — GIL отпускается
def fetch(url: str) -> int:
import requests
return requests.get(url, timeout=5).status_code
urls = ["https://example.com"] * 8
t = time.perf_counter()
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(fetch, urls))
print(f"IO threads: {time.perf_counter() - t:.2f}s", results)
# CPU-bound в C-extension: hashlib отпускает GIL
DATA = b"x" * (1 << 24) # 16 MiB
def hash_chunk(_: int) -> str:
return hashlib.sha256(DATA).hexdigest()
t = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(hash_chunk, range(8)))
print(f"hashlib threads: {time.perf_counter() - t:.2f}s")
# Время близко к 1/N от однопоточного — GIL отпущен внутри hashlib
Как проверить
- Профайлинг:
py-spy --idle --threadsпокажет, ждут ли потоки I/O. sys.getswitchinterval()— текущий quantum (по умолчанию 5 мс).- В нативных библиотеках смотрите наличие
nogil/Py_BEGIN_ALLOW_THREADSв исходниках.
Подводные камни
- Списывать высокую latency БД на GIL без профайлинга — почти всегда виновата сама БД или сеть.
- Считать, что любой C-код освобождает GIL — нет, только тот, который явно это делает.
- CPU-heavy callback в I/O thread pool ломает картину: GIL держится, остальные I/O потоки лагают.
- На free-threaded build (3.13+) GIL ослаблен, но shared state требует явных lock-ов — code, который раньше «работал», начнёт race-ить.
- multiprocessing спасает от GIL, но добавляет IPC overhead (pickle), shared-memory сложность и fork-проблемы на macOS/Windows.
Common mistakes
- Говорить, что GIL всегда bottleneck.
- Не упоминать C-extensions.
- Не предлагать профилирование.
What the interviewer is testing
- Называет I/O-bound сценарии.
- Знает про native extensions.
- Понимает необходимость измерений.