Один важный момент: всё написанное здесь относится к CPython — стандартной реализации Python, которую вы, скорее всего, и используете. Jython и IronPython устроены иначе, там GIL нет. Но в 99% случаев «Python» в продакшне — это CPython.
Перед тем как двигаться дальше, разберём несколько терминов, которые встретятся по тексту.
Segfault (Segmentation Fault)
Программа попыталась обратиться к памяти, которой не владеет — например, к уже освобождённому объекту. Операционная система немедленно убивает процесс сигналом SIGSEGV. В Python это редкость — но именно так упал бы интерпретатор без GIL, если два потока одновременно освободили один и тот же объект.
# Без GIL: thread1 видит refcount=1 -> думает "я последний"# thread2 одновременно тоже видит refcount=1# оба делают free(obj) -> второй free = segfault
Опкод (opcode)
Элементарная инструкция виртуальной машины Python. Когда интерпретатор запускает ваш код, он сначала компилирует его в последовательность опкодов — байткод. Каждый опкод — это одно атомарное действие: загрузить переменную, сложить два числа, вызвать функцию. Посмотреть опкоды любой функции можно через модуль dis.
Reference counting
Механизм управления памятью в CPython. Каждый объект хранит счётчик ob_refcnt — сколько мест на него ссылается. Когда счётчик падает до нуля, объект не��едленно уничтожается. Это быстро и предсказуемо, но требует защиты от одновременного изменения счётчика из разных потоков — именно для этого и нужен GIL.
Мьютекс (mutex)
Mutual Exclusion — примитив синхронизации, который позволяет только одному потоку находиться в защищённой секции кода одновременно. Поток «берёт» (acquires) мьютекс, делает работу, «отдаёт» (releases). Все остальные потоки в это время ждут. GIL — это и есть мьютекс, но глобальный: он защищает весь интерпретатор целиком, а не отдельный ресурс.
Race condition
Состояние гонки — баг, при котором результат зависит от того, в каком порядке потоки успеют выполнить свои операции. Воспроизводится непредсказуемо: иногда всё работает, иногда нет. GIL не защищает от race condition полностью — он лишь делает отдельные опкоды атомарными, но не целые выражения.
Байткод (bytecode)
Промежуточное представление Python‑кода — набор опкодов, который выполняет виртуальная машина CPython. Когда вы запускаете .py файл, Python сначала компилирует его в байткод (кеширует в .pyc), а затем интерпретирует. Именно выполнение байткода и защищает GIL — только один поток может исполнять опкоды в каждый момент времени.
Context switch (переключение контекста)
Операция ОС: сохранить состояние текущего потока (регистры, стек, счётчик инструкций), загрузить состояние другого и передать управление ему. Это не бесплатно — занимает микросекунды и «портит» CPU‑кеш. Слишком частые переключения — GIL thrashing — могут замедлить многопоточный код сильнее, чем однопоточный.
Что такое GIL и зачем он нужен
GIL — это мьютекс внутри CPython, который гарантирует: в каждый момент времени только один поток выполняет Python‑байткод. Не «только один поток запущен» — потоков может быть сколько угодно, но байткод исполняет лишь один.
Зачем это сделали? В Python каждый объект имеет счётчик ссылок (ob_refcnt). Когда он падает до нуля — объект уничтожается. Без GIL два потока могут одновременно декрементировать этот счётчик, получить race condition и либо освободить память раньше времени (segfault), либо не освободить никогда (утечка). GIL — это грубый, но дешёвый способ сделать управление памятью thread‑safe.
Дополнительно: GIL упрощает интеграцию C‑расширений, которые не thread‑safe по природе.
Как GIL работает изнутри
В CPython 3.2+ появился новый механизм GIL (Gil Shedding). До этого был старый — на основе sys.checkinterval (количество опкодов).
Новый GIL (3.2+):
Есть таймаут
sys.getswitchinterval()— по умолчанию 5 миллисекунд.Поток держит GIL, выполняет байткод.
Через 5 мс другой поток выставляет флаг
gil_drop_request = 1.Текущий поток видит флаг, сбрасывает GIL, другой поток его захватывает.
Если других желающих нет — тот же поток берёт GIL обратно.
Важно: переключение происходит не между опкодами, а по таймеру. Это значит, что поток может выполнить тысячи опкодов за 5 мс без переключения.

sys.setswitchinterval() — зачем и когда менять
Это таймаут в секундах, после которого ожидающий поток может запросить GIL у текущего. По умолчанию — 5 мс.
На практике setswitchinterval трогают редко. Один реальный кейс: CPU‑bound воркеры с threading + NumPy — там увеличение интервала до 50–100 мс снижает overhead на переключения, потому что NumPy сам отдаёт GIL когда надо. Для I/O‑сервисов — не трогать.
Как устроен GIL внутри CPython
Здесь опишу основы, которых хватит для прохождения большинства интервью, так как полный разбор внутренностей GIL заслуживает отдельной статьи и уже есть на Хабр.
Внутри CPython GIL — это структура _gil_runtime_state в Python/ceval_gil.c. Ключевые поля:
locked — атомарный int, 0 или 1. Взят GIL или нет.
mutex + cond_var — стандартные POSIX примитивы. Ожидающий поток засыпает на cond_var, просыпается когда текущий держатель делает pthread_cond_signal.
eval_breaker — атомарный флаг, который проверяется в главном цикле интерпретатора (ceval.c) после каждого опкода. Если выставлен — поток смотрит почему: нужно ли отдать GIL, обработать сигнал или выполнить что‑то ещё.
gil_drop_request — отдельный флаг, который ожидающий поток выставляет когда хочет GIL. Когда текущий поток видит его через eval_breaker — отдаёт GIL и ждёт на cond_var, пока другой поток его не освободит обратно (чтобы не взять GIL снова раньше ожидающего).
Это важный нюанс: после release текущий поток не берёт GIL сразу обратно — он ждёт, чтобы дать шанс другому. Без этого один активный поток мог бы монополизировать GIL навсегда.
Почему нужны Lock/RLock даже с GIL
GIL гарантирует атомарность одного опкода. Но почти любое полезное действие — это несколько опкодов.
# Выглядит как одна операция — на самом деле три counter += 1 # Байткод: # LOAD_GLOBAL counter <- поток может быть прерван ЗДЕСЬ # LOAD_CONST 1 # BINARY_ADD # STORE_GLOBAL counter <- и здесь
GIL может переключиться между любыми двумя опкодами. Если два потока делают counter += 1 одновременно — оба прочитают одно значение, оба прибавят 1, оба запишут одно и то же. Итог: вместо +2 получаем +1.
Lock — про��той мьютекс. Один поток держит, остальные ждут.
RLock (Reentrant Lock) — тот же поток может взять его несколько раз без дедлока. Нужен когда защищённый метод вызывает другой защищённый метод того же объекта.
import threading counter = 0 lock = threading.Lock() def safe_increment(): global counter with lock: # acquire counter += 1 # теперь атомарно для нашей логики # release — автоматически # RLock — когда метод вызывает метод class SafeTree: def __init__(self): self._lock = threading.RLock() def insert(self, value): with self._lock: # берёт RLock первый раз self._rebalance() # вызывает метод, который тоже берёт RLock def _rebalance(self): with self._lock: # берёт RLock второй раз — не дедлок pass
Правило простое: если несколько потоков читают и пишут одни данные — нужен Lock, независимо от GIL. GIL защищает интерпретатор, а не вашу бизнес‑логику.
CPU‑bound vs I/O‑bound — ключевое разграничение
Это самая важная тема на собеседовании. Нужно понимать не просто «GIL плохой» — а когда он проблема, а когда нет.
I/O‑bound задачи (сеть, диск, БД): GIL не проблема. Когда поток уходит в системный вызов (read, write, select), он сам отдаёт GIL. То есть пока один поток ждёт ответа от Postgres, другой спокойно работает. threading и asyncio отлично справляются.
CPU‑bound задачи (вычисления, парсинг, шифрование): GIL — реальная проблема. Два потока, которые считают числа, не дают друг другу параллельно использовать CPU. Реальное ускорение от многопоточности на CPU‑bound = 0 или даже хуже из‑за overhead на переключение.
# I/O-bound — threading работает хорошо import threading, requests def fetch(url): r = requests.get(url) # GIL отдаётся во время ожидания ответа return r.status_code threads = [threading.Thread(target=fetch, args=(url,)) for url in urls] # CPU-bound — threading НЕ помогает def heavy_compute(n): return sum(i * i for i in range(n)) # GIL держится всё время # Решение для CPU-bound: from multiprocessing import Pool with Pool(4) as p: results = p.map(heavy_compute, [10**7]*4) # каждый процесс — свой GIL
Когда C-расширения освобождают GIL
Это важный нюанс, который спрашивают на senior‑уровне. NumPy, pandas, cryptography, lxml — все они в CPU‑тяжёлых операциях явно освобождают GIL с помощью макросов Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS.
// Так выглядит освобождение GIL в C-коде NumPy Py_BEGIN_ALLOW_THREADS // вот здесь идёт тяжёлое вычисление на чистом C matrix_multiply(a, b, result, n); Py_END_ALLOW_THREADS
Вывод: numpy в многопоточном коде даёт реальный параллелизм, потому что сама тяжёлая работа выполняется без GIL. Поэтому threading + numpy — это нормально. Но threading + чистый Python код на CPU — нет.
asyncio и GIL
asyncio не обходит GIL — он просто не нуждается в параллелизме. Это кооперативная многозадачность в одном потоке. Пока одна корутина ждёт I/O, event loop переключается на другую. GIL при этом вообще не является ограничением, потому что поток один.
import asyncio async def fetch_data(session, url): async with session.get(url) as resp: # здесь происходит yield to event loop return await resp.json() async def main(): async with aiohttp.ClientSession() as session: tasks = [fetch_data(session, url) for url in urls] results = await asyncio.gather(*tasks) # 1000 запросов параллельно, 1 поток
Разница threading vs asyncio для I/O:
threading: реальные OS‑потоки, overhead на создание и переключение, легко получить race conditionasyncio: один поток, нет overhead, явное управление переключением, нет гонок по данным (но нельзя использовать блокирующие функции)
Thread safety, process safety, async safety
Три разных контекста — три разных проблемы.
тип | проблема | общая память | инструменты защиты | где встречается |
Thread safety | race condition между потоками одного процесса | да — напрямую |
|
|
Process safety | гонки при доступе к общим ресурсам между процессами | только явно |
|
|
Async safety | гонка между корутинами при await — другая корутина меняет состояние пока ты ждёшь | один поток |
|
|
Важный нюанс по async safety — там нет параллелизма, но гонки всё равно возможны:
# Небезопасно в asyncio — между двумя await другая корутина может изменить баланс async def transfer(from_acc, to_acc, amount): balance = await db.get_balance(from_acc) # await — точка переключения if balance >= amount: # ЗДЕСЬ другая корутина может тоже прочитать тот же баланс await db.deduct(from_acc, amount) await db.add(to_acc, amount) # Безопасно — транзакция на уровне БД или asyncio.Lock async def transfer_safe(from_acc, to_acc, amount): async with account_lock: balance = await db.get_balance(from_acc) if balance >= amount: await db.deduct(from_acc, amount) await db.add(to_acc, amount)
Каждый await — это точка где event loop может переключиться на другую корутину. Всё что между двумя await — атомарно. Всё что пересекает await — нет.
Альтернативы CPython без GIL
multiprocessing — стандартное решение. Каждый процесс — своя копия интерпретатора и свой GIL. Общение через Queue, Pipe, shared memory. Overhead на сериализацию (pickle) между процессами.
Jython, IronPython — нет GIL, есть реальная многопоточность. Но нет большинства C‑расширений, включая NumPy.
PyPy — есть GIL (как в CPython), но JIT‑компиляция делает однопоточный код в 5–10x быстрее.
Python 3.13 — nogil build (PEP 703) — экспериментальная сборка CPython без GIL. Использует per‑object lock вместо глобального. Пока что медленнее обычного CPython в однопоточных сценариях (~5–10% overhead), но даёт реальный параллелизм. В продакшн не идёт.
subinterpreters (PEP 554) — несколько интерпретаторов в одном процессе, у каждого свой GIL. В Python 3.12 добавлен interpreters модуль. Перспективно, но API ещё нестабильный.
Как мы решаем это в хайлоад
На практике в финансовом сервисе картина такая:
HTTP‑сервис (FastAPI/aiohttp): asyncio + uvloop. GIL не мешает вообще — всё I/O‑bound: запросы к БД, кешу, внешним API. Тысячи конкурентных запросов в одном потоке.
CPU‑тяжёлые задачи (risk calculations, ML inference): выносим в отдельные workers через Celery + multiprocessing. Либо используем NumPy/pandas (которые сами освобождают GIL) в thread pool.
Streaming/real‑time: Kafka consumer в отдельном процессе, обработка через ProcessPoolExecutor если CPU‑bound.
# Правильный паттерн для смешанной нагрузки from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor # CPU-bound: отдельные процессы with ProcessPoolExecutor(max_workers=4) as executor: risk_results = executor.map(calculate_risk, positions) # I/O-bound: потоки (или asyncio) with ThreadPoolExecutor(max_workers=20) as executor: db_results = executor.map(fetch_from_db, query_ids)
Concurrency vs Parallelism
Это самая частая путаница в теме многопоточности. Термины звучат похоже, но описывают принципиально разные вещи.
Concurrency — про структуру программы. Несколько задач существуют одновременно и могут прогрессировать, перемежая выполнение. Не обязательно в один момент времени — достаточно того, что они не ждут друг друга последовательно.
Parallelism — про физическое исполнение. Несколько задач буквально выполняются в одну и ту же наносекунду на разных ядрах процессора.
Классическая формулировка от Роба Пайка: concurrency — это про работу с несколькими вещами одновременно, parallelism — про выполнение нескольких вещей одновременно. Concurrency — это дизайн. Parallelism — это рантайм.

Короткая формула для запоминания: parallelism — это всегда concurrency, но concurrency — не всегда parallelism. Можно иметь concurrency без parallelism (один поток переключается между задачами). Нельзя иметь parallelism без concurrency (параллельные задачи по определению concurrent).
Где находятся asyncio, threading и multiprocessing
Теперь по каждому детально.
asyncio — concurrency без parallelism
Один поток, один GIL, одно ядро. Задачи не выполняются одновременно физически — они чередуются в точках await. Пока одна корутина ждёт сеть, event loop переключается на другую. Это кооперативная многозадачность: задачи сами решают когда отдать управление.
Parallelism здесь невозможен по определению — поток один. Но для I/O‑bound задач это не нужно: CPU всё равно простаивает пока ждёт ответа от сети.
threading — concurrency, parallelism зависит от контекста
Несколько OS‑потоков — concurrency есть. Но GIL не даёт двум потокам выполнять Python‑байткод одновременно — parallelism для чистого Python‑кода отсутствует. Это и есть суть проблемы.
Исключение: если поток выполняет C‑расширение, которое явно освобождает GIL (NumPy, cryptography), или ждёт I/O системный вызов — в этот момент другой поток может работать параллельно. parallelism есть, но частичный и зависит от того, что делают потоки.
multiprocessing — concurrency и parallelism одновременно
Несколько процессов, у каждого своя копия интерпретатора и свой GIL. Никакого общего GIL нет — процессы реально выполняются на разных ядрах одновременно. Это единственный способ получить настоящий parallelism для чистого Python‑кода.
Цена — нет общей памяти. Данные между процессами передаются через сериализацию (pickle), что создаёт overhead. Для небольших данных и долгих задач это незаметно. Для задач где нужно часто передавать большие объёмы — боль.
Каверзные вопросы и ответы
«Многопоточность в Python бесполезна из-за GIL» — вы согласны?
Нет. Это упрощение. Threading полезен для I/O‑bound задач — а это 90% нагрузки типичного веб‑сервиса. Пока поток ждёт сеть или диск, он отдаёт GIL и другой поток работает. Бесполезен threading только для CPU‑bound Python‑кода. NumPy и другие C‑расширения явно освобождают GIL, поэтому threading + numpy даёт реальный параллелизм.
Можно ли получить race condition в Python несмотря на GIL?
Да, легко. GIL гарантирует атомарность отдельных байткод‑операций, но не атомарность последовательностей операций. Например, x += 1 — это LOAD, ADD, STORE: три опкода. GIL может переключиться между ними. Классический пример: два потока делают counter += 1 по 100 000 раз, а в итоге counter < 200000. Решение — threading.Lock(), queue.Queue, или atomic типы (например, из atomics библиотеки).
Что произойдёт, если убрать GIL из CPython прямо сейчас?
Нужно будет заменить единый GIL per‑object locks для каждого объекта — иначе reference counting сломается. Это:
1) overhead на каждую операцию с объектами,
2) риск дедлоков,
3) поломка всех C‑расширений, которые предполагают наличие GIL. Именно это и реализует PEP 703 (nogil build в 3.13) — и в однопоточных сценариях это даёт ~5–10% деградацию. Также все C‑расширения нужно пересобрать с учётом thread safety.
asyncio использует несколько потоков?
Нет. По умолчанию asyncio работает в одном потоке. Event loop кооперативно переключается между корутинами. GIL вообще не является ограничением — поток один. Исключение: loop.run_in_executor() запускает блокирующий код в ThreadPoolExecutor или ProcessPoolExecutor — вот там уже вступает в игру GIL (для threads) или отсутствие общей памяти (для processes).
Почему иногда многопоточный код работает МЕДЛЕННЕЕ однопоточного?
Несколько причин:
1. GIL thrashing — потоки постоянно соревнуются за GIL, тратя время на ожидание и переключение контекста.
2. False sharing в CPU кешах — когда разные потоки работают с данными в одной cache line.
3. Overhead на создание потоков и синхронизацию. Для CPU‑bound задач с маленьким объёмом работы на поток — многопоточность даёт чистый минус.
Как coroutine в asyncio отдаёт управление event loop?
Через await. Когда корутин доходит до await some_io(), он возвращает управление event loop (буквально — yield управление). Event loop регистрирует callback на I/O событие (через epoll/kqueue) и переключается на следующую готовую корутину. Когда I/O завершается — корутин возобновляется с того же места. Если в корутине нет await — он держит event loop заблокированным, как если бы это был один большой синхронный вызов. Это называется blocking the event loop и это распространённый баг.
Чем отличается GIL в Python 2 и Python 3?
В Python 2 GIL переключался каждые N байткод‑инструкций (по умолчанию 100, sys.setcheckinterval). Это создавало проблему: на multicore CPU потоки могли «биться» — один поток пытался взять GIL, сразу получал отказ, снова пытался — создавая convoy effect.
В Python 3.2 Antoine Pitrou переписал GIL на таймер (5ms), добавил механизм явного запроса GIL от ожидающего потока. Это убрало convoy effect и сделало переключение более предсказуемым.
Как проверить, является ли операция атомарной в Python?
Через dis модуль — посмотреть на байткод. Одна байткод‑инструкция атомарна с точки зрения GIL (переключение происходит между инструкциями). Атомарны: list.append(), list.pop(), присваивание переменной (STORE_NAME). Неатомарны: x += 1 (три инструкции), dict[key] = dict.get(key, 0) + 1. Но полагаться на это в продакшн‑коде — плохая практика: детали реализации могут измениться. Используйте threading.Lock явно.
GIL — не баг и не ошибка проектирования. Это осознанный компромисс, который Гвидо ван Россум принял в 1992 году: простота реализации и производительность в однопоточном режиме в обмен на ограничения многопоточности. Тогда многоядерных процессоров в массовом доступе не существовало. Решение было разумным.
Проблема в том, что мир изменился, а GIL остался.
Сегодня, в 2026 году, у дата‑сайентиста в ноутбуке 12 ядер. Финансовый сервис обрабатывает 50 000 транзакций в секунду. ML‑модели считают на кластерах из сотен машин. И всё это — на Python, который физически не может использовать больше одного ядра для чистого Python‑кода одновременно.
При этом Python продолжает расти. Каждый год — первое место в рейтингах популярности. Парадокс: самый популярный язык с одним из самых неудобных ограничений для параллельных вычислений.
Работа над устранением GIL идёт. PEP 703 в Python 3.13 — это не эксперимент ради эксперимента, это результат многолетних попыток решить задачу, которую сам Гвидо когда‑то назвал почти невозможной. Посмотрите, как развивается дискуссия в core dev community — там до сих пор нет консенсуса, насколько допустима деградация однопоточной производительности ради настоящего параллелизма.
Практический итог для тех, кто пишет продакшн‑код прямо сейчас: знание GIL отделяет людей, которые понимают, почему их код медленный, от тех, кто добавляет потоки наугад и удивляется результату. Это не академическая тема — это то, что напрямую влияет на архитектурные решения, которые вы принимаете каждый день.
