Конкурентность и GIL
Под словом «конкурентность» в Python скрываются три разные машины — потоки ОС, отдельные процессы и однопоточный событийный цикл, — и выбор между ними определяется не вкусом, а тем, на что программа тратит время. Поверх всех трёх лежит GIL, самый пересказываемый и хуже всего понятый механизм языка. Точная формулировка — GIL это деталь реализации CPython, а не свойство языка Python: в Jython и IronPython его нет вовсе. Он защищает внутреннее состояние интерпретатора, в первую очередь подсчёт ссылок, и сериализует только исполнение байт-кода. Вокруг блокирующего I/O и внутри многих C-расширений он отпускается — поэтому потоки прекрасно ускоряют сетевую работу и бесполезны на чистом Python-цикле.
Отдельно проговорите версионность — заявление «в Python всегда есть GIL» уже неверно. Python 3.12 (PEP 684) дал каждому субинтерпретатору собственный GIL, а Python 3.13 (PEP 703) ввёл опциональную free-threaded сборку, где GIL выключается целиком. Классическая модель с одним общим GIL остаётся ответом по умолчанию, но называйте её версионно-зависимой, а не вечной. Дальше идут ловушки исполнения — гонка на counter += 1, взаимная блокировка на двух Lock, блокирующий вызов, застопоривший событийный цикл, и цена pickle на границе процессов.
Карта темы
- GIL — что он на самом деле защищает — мьютекс интерпретатора, подсчёт ссылок, что сериализуется, когда отпускается и версионность.
- I/O-bound против CPU-bound — классификация нагрузки по тому, где уходит время; она и определяет модель.
- Потоки — threading — настоящие потоки ОС, гонки на неатомарных операциях,
Lockи взаимные блокировки. - Поток против процесса — общее адресное пространство против изоляции, цена создания и обмена данными.
- Процессы — multiprocessing — свой интерпретатор и свой
GILна процесс, способы стартаfork/forkserver/spawn, picklable. - Корутины — подпрограмма с несколькими точками входа и объект корутины, который сам по себе ничего не делает.
- async/await и событийный цикл — кооперативная многозадачность в одном потоке и один блокирующий вызов, ломающий её.
- Зелёные потоки —
greenlet/gevent, неявное переключение через monkey-patching против явногоawait. - Выбор модели конкурентности — решающая таблица и разбор задач на миллион запросов.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
«GIL — часть языка Python» | В Jython и IronPython его нет; это ограничение реализации CPython, и с 3.13 его можно отключить сборкой |
«GIL делает код потокобезопасным» | Он гарантирует атомарность байт-кода, но не составной операции — counter += 1 теряет обновления |
«GIL не даёт потокам ускорить ничего» | На I/O и внутри C-расширений он отпускается — три потока time.sleep(1) завершатся за 1 секунду, а не за 3 |
| Добавить потоков на CPU-задачу «на всякий случай» | Ускорения не будет, а расходы на передачу GIL делают программу медленнее однопоточной в разы |
Считать asyncio многопоточным | Событийный цикл живёт в одном потоке; любой синхронный блокирующий вызов останавливает все корутины |
Вызвать async def-функцию и не дождаться её | Получается объект корутины, тело не исполняется, а RuntimeWarning о never awaited придёт лишь на сборке мусора |
Полагаться на память родителя после fork | Способ старта зависит от платформы и версии; при spawn/forkserver модуль импортируется заново и изменения состояния теряются |
| Запускать неограниченное число одновременных запросов | Кончаются не память и не CPU, а сокеты и дескрипторы — нужен asyncio.Semaphore или пул |
Значение для собеседований
Тема входит в обязательную программу любого Python-собеседования от middle и почти всегда начинается с вопроса «что такое GIL». Проходной ответ содержит четыре элемента — это мьютекс CPython, он защищает внутренности интерпретатора и подсчёт ссылок, он сериализует только байт-код, и он отпускается на блокирующем I/O. Ответ «GIL запрещает многопоточность» сразу переводит разговор в разбор ошибок — следующим будет вопрос про три потока, ждущих сеть. Второй обязательный блок — классификация нагрузки: вам покажут код и спросят, ускорят ли его потоки.
Дальше проверяют исполнение. Классический код-вопрос — потоковый счётчик, теряющий инкременты, где ждут слов «read-modify-write» и «не атомарно на уровне байт-кода», а не просто «добавь Lock». Второй — две взаимные блокировки с диагнозом «инверсия порядка захвата». Третий — блокирующий вызов внутри корутины, где ждут asyncio.to_thread. На senior-уровне добавляются вопросы на пределы — могут ли потоки сделать CPU-задачу медленнее однопоточной (да, из-за передачи GIL) и какой ресурс кончится первым на миллионе запросов (сокеты и дескрипторы, а не CPU и не память). Типичная ошибка на всех уровнях одна — кандидат называет модель раньше, чем классифицировал нагрузку.