Кто держит блокировку, кто её ждёт и кто работает без неё — по трём типам нагрузки, рядом с настоящими замерами.
О GIL написаны сотни статей, и большинство из них повторяет одну ошибку: считает, что GIL делает потоки бесполезными. Он делает бесполезным ровно один сценарий — чистый Python, упирающийся в процессор. Ниже видно, чем эти три случая отличаются: диаграмма — модель поведения, числа рядом с ней — настоящие замеры на этой машине.
Первая диаграмма показывает пропускную способность. Есть эффект, который она не показывает, и на проде он болезненнее: поток, ждущий ответа сети, не получит управление раньше, чем истечёт интервал переключения занятого потока. Один CPU-bound поток в процессе — и латентность всего веб-сервиса привязывается к этому числу.
Замер: один поток крутит пустой цикл, второй делает time.sleep(0.001) и меряет, насколько поздно проснулся. Здесь всё настоящее, без модели.
import sys, time, threading, statistics
sys.setswitchinterval(0.005) # значение по умолчанию
ev = threading.Event()
lat = []
def burner(): # один поток занимает GIL
x = 0
while not ev.is_set():
x += 1
def responder(): # второй пытается быстро просыпаться
for _ in range(80):
t0 = time.perf_counter()
time.sleep(0.001)
lat.append((time.perf_counter() - t0 - 0.001) * 1000)
b = threading.Thread(target=burner, daemon=True); b.start()
r = threading.Thread(target=responder); r.start(); r.join()
ev.set()
print("медиана опоздания, мс: %.2f" % statistics.median(lat))
# → около 5 мс: ровно интервал переключения
# без потока-burner то же измерение даёт 0.09 мс