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.
  • Понимает необходимость измерений.

Sources

Related topics