GIL в Python простыми словами: как работает и что спрашивают на собеседовании
Разбираем Global Interpreter Lock с нуля: история появления, bytecode-переключение, влияние на threading/multiprocessing, обход GIL и free-threaded Python 3.13.
- 1. Откуда взялся GIL: история и причины
- 1.1 CPython и подсчёт ссылок
- 1.2 Решение 1992 года
- 1.3 Почему не убрали до сих пор
- 2. Как GIL работает изнутри
- 2.1 Eval loop и байткод
- 2.2 Механизм переключения
- 2.3 Check interval: от тиков к секундам
- 2.4 I/O освобождает GIL
- 2.5 Диаграмма работы двух потоков
- 3. Влияние GIL на CPU-bound и I/O-bound задачи
- 3.1 CPU-bound: threading не помогает
- 3.2 I/O-bound: threading прекрасно работает
- 3.3 Итоговая таблица
- 4. Когда threading всё-таки работает
- 4.1 I/O-сценарии на практике
- 4.2 Имитация I/O через time.sleep
- 4.3 Ограничения threading
- 5. Как обойти GIL: три реальных подхода
- 5.1 multiprocessing: отдельные процессы
- 5.2 C-расширения: явное освобождение GIL
- 5.3 Альтернативные интерпретаторы
- 6. Free-threaded Python 3.13
- 6.1 PEP 703: Making the GIL Optional
- 6.2 Как попробовать
- 6.3 Реальный параллелизм
- 6.4 Компромиссы и статус
- 7. Типичные вопросы на собеседовании с ответами
- Что такое GIL и зачем он нужен?
- GIL и потоки: можно ли ускорить CPU-bound программу потоками?
- Почему I/O-bound задачи ускоряются с threading несмотря на GIL?
- Как обойти GIL для CPU-bound задач?
- Чем ProcessPoolExecutor отличается от ThreadPoolExecutor?
- Что такое check interval?
- Влияет ли GIL на asyncio?
- Почему не убирают GIL?
- NumPy и GIL: как они связаны?
- Что такое free-threaded Python?
- Что происходит с GIL при вызове C-расширения?
- Как написать параллельную обработку данных на Python?
- 8. Итоги: выбор инструмента
GIL — аббревиатура, которую рано или поздно произносит каждый Python-разработчик на собеседовании. Global Interpreter Lock — это не баг, не артефакт плохого дизайна и не временная заглушка. Это осознанное архитектурное решение, принятое в 1992 году и живущее в CPython до сих пор. Понять, почему оно появилось, как работает изнутри и что реально ограничивает, — значит ответить на добрую треть вопросов по многопоточности на Python-интервью.
На junior-уровне достаточно знать определение: «GIL — мьютекс, разрешающий только одному потоку выполнять Python-байткод в каждый момент». Но middle-разработчику интервьюер задаёт уточнения: «Почему threading не ускоряет CPU-bound задачи?», «Как NumPy обходит GIL?», «Что изменилось в Python 3.13?». Именно эти вопросы мы и разберём здесь — от истории появления до практических паттернов и free-threaded режима.
Если вы ещё не смотрели общий обзор, загляните в Python: 50 вопросов на собеседовании junior–middle — там GIL упоминается в разделе «Управление памятью», а здесь мы уходим на уровень глубже.
Python на собеседовании — без стресса
50 вопросов с разбором: GIL, asyncio, ООП и память.
1. Откуда взялся GIL: история и причины
1.1 CPython и подсчёт ссылок
Чтобы понять GIL, нужно сначала понять, как CPython управляет памятью. Каждый объект в CPython хранит счётчик ссылок — ob_refcnt. Когда вы присваиваете переменную, передаёте объект в функцию или кладёте его в список, счётчик инкрементируется. Когда ссылка выходит из области видимости — декрементируется. Когда счётчик падает до нуля, объект немедленно удаляется.
Это эффективно: нет отдельного GC-цикла для большинства объектов (он есть только для циклических ссылок). Но есть проблема. Инкремент и декремент ob_refcnt — не атомарные операции на уровне железа. Если два потока одновременно манипулируют одним объектом, счётчик может оказаться неверным: объект удалится слишком рано или вообще не удалится, оставшись висеть в памяти навсегда.
import sys
x = []
sys.getrefcount(x) # 2: переменная x + аргумент getrefcount
y = x
sys.getrefcount(x) # 3: x + y + аргумент getrefcount
del y
sys.getrefcount(x) # 2: снова
Без защиты такой код в многопоточной среде мог бы привести к use-after-free и падению интерпретатора.
1.2 Решение 1992 года
Гвидо ван Россум добавил GIL в Python в 1992 году, когда портировал интерпретатор на Windows, которая тогда появилась с поддержкой потоков. Логика была простой: вместо того чтобы защищать каждый счётчик ссылок отдельным мьютексом (что дорого и сложно), защищаем весь интерпретатор одним глобальным замком.
Это решение работало отлично для однопроцессорных машин эпохи 90-х — всё равно только одно ядро, так что GIL не создавал никаких потерь. Проблема проявилась позже, когда многоядерные процессоры стали нормой.
1.3 Почему не убрали до сих пор
Причин несколько, и каждая весома.
C-расширения. Тысячи библиотек написаны на C и используют Python C API. Этот API предполагает, что GIL защищает структуры интерпретатора. Убрать GIL без обратной совместимости — значит сломать NumPy, SciPy, lxml, PIL и половину экосистемы.
Производительность однопоточного кода. Fine-grained locking (отдельный мьютекс на каждый объект) медленнее глобального замка из-за накладных расходов на lock/unlock. Без GIL однопоточный Python был бы заметно медленнее — примерно на 20-30% по ранним оценкам.
Сложность реализации. JVM решала аналогичную задачу годами, имея огромную команду инженеров. Для CPython, который разрабатывается сообществом добровольцев, это колоссальный объём работы.
Для сравнения: JVM использует fine-grained locking с volatile-полями и CAS-операциями. Ruby MRI имел собственный GIL вплоть до Ruby 3.0, где его заменили на Ractors. Python пошёл другим путём — добавил экспериментальный free-threaded режим в 3.13.
2. Как GIL работает изнутри
2.1 Eval loop и байткод
CPython выполняет код в главном цикле — ceval.c::_PyEval_EvalFrameDefault. Интерпретатор дизассемблирует Python-код в байткод и выполняет инструкцию за инструкцией в этом цикле.
import dis
def add(a, b):
return a + b
dis.dis(add)
# RESUME 0
# LOAD_FAST 0 (a)
# LOAD_FAST 1 (b)
# BINARY_OP 0 (+)
# RETURN_VALUE
GIL защищает именно этот цикл: в каждый момент только один поток может исполнять байткод-инструкции.
2.2 Механизм переключения
В исходниках CPython GIL реализован через переменную _Py_atomic_int gil_locked и функции take_gil() / drop_gil(). Когда поток хочет начать исполнение, он вызывает take_gil() — блокируется до тех пор, пока GIL не освободится. После выполнения фрагмента — drop_gil().
Чтобы GIL передавался между потоками справедливо, интерпретатор периодически проверяет, есть ли другие потоки, ожидающие замка. Эта проверка происходит через check interval.
2.3 Check interval: от тиков к секундам
В Python 2 и ранних версиях Python 3 check interval измерялся в «тиках» — количестве байткод-инструкций. sys.getcheckinterval() возвращало 100 по умолчанию: каждые 100 инструкций интерпретатор проверял, нужно ли передать GIL.
С Python 3.2 это изменилось. Антуан Питру переделал механизм: теперь sys.getswitchinterval() возвращает время в секундах (0.005 по умолчанию — 5 миллисекунд). Поток, держащий GIL, будет вытеснен через 5 мс, если другой поток ждёт.
import sys
print(sys.getswitchinterval()) # 0.005
# Можно изменить (обычно не нужно)
sys.setswitchinterval(0.001) # 1 мс — более агрессивное переключение
Старый API sys.getcheckinterval() убран в Python 3.2. Это частый вопрос на интервью: «Как изменился check interval в Python 3.2?»
2.4 I/O освобождает GIL
Ключевой факт, который объясняет, почему threading полезен для I/O-задач: любая блокирующая I/O-операция явно освобождает GIL перед тем, как уйти в ожидание, и захватывает обратно после завершения.
В C API это выглядит так:
// В исходниках CPython (упрощённо):
Py_BEGIN_ALLOW_THREADS // освобождает GIL
result = recv(fd, buf, len, flags); // системный вызов
Py_END_ALLOW_THREADS // захватывает GIL обратно
Это значит: пока один поток ждёт ответ от сети или диска, другой поток может исполнять байткод. GIL не блокирует конкурентный I/O — он блокирует только конкурентное выполнение Python-кода.
2.5 Диаграмма работы двух потоков
Представим двух потоков — Thread-1 и Thread-2:
Thread-1: [==исполнение байткода==][drop_gil] [take_gil][==исполнение==]...
Thread-2: [waiting for GIL...][take_gil][==исполнение байткода==]...
──────────────────────────────────────────────────────────────────► время
Thread-1 держит GIL, выполняет байткод. Через 5 мс (или при I/O) освобождает. Thread-2, который ждал, захватывает GIL и начинает выполнение. Thread-1 уходит в очередь. По очереди, не параллельно — в этом суть GIL.
3. Влияние GIL на CPU-bound и I/O-bound задачи
3.1 CPU-bound: threading не помогает
CPU-bound задача — та, где процессор занят вычислениями без ожидания. Вычисление факториала, хэшей, обработка изображений в чистом Python. Здесь GIL создаёт настоящую проблему.
Запустим два потока на числодробилке:
import threading
import time
def cpu_task(n):
total = 0
for i in range(n):
total += i * i
return total
N = 10_000_000
# Последовательно
start = time.time()
cpu_task(N)
cpu_task(N)
print(f"Sequential: {time.time() - start:.2f}s")
# Параллельно (threading)
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(N,))
t2 = threading.Thread(target=cpu_task, args=(N,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threading: {time.time() - start:.2f}s")
# Результат: Threading работает примерно так же медленно или медленнее
Почему медленнее? Потому что потоки не работают параллельно — GIL допускает только один за раз. Плюс накладные расходы на постоянный take_gil() / drop_gil() и переключение контекста. В итоге threading на CPU-bound даёт нулевой или отрицательный эффект.
3.2 I/O-bound: threading прекрасно работает
I/O-bound задача — сетевые запросы, чтение файлов, ожидание базы данных. Здесь GIL освобождается на время ожидания, и потоки реально работают конкурентно:
import threading
import time
import urllib.request
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
def fetch(url):
urllib.request.urlopen(url)
# Последовательно: ~4 секунды
start = time.time()
for url in urls:
fetch(url)
print(f"Sequential: {time.time() - start:.1f}s")
# Threading: ~1 секунда
start = time.time()
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
print(f"Threading: {time.time() - start:.1f}s")
Пока один поток ждёт ответа сервера, GIL свободен. Другие потоки занимают его и тоже отправляют запросы. В итоге четыре параллельных запроса занимают примерно столько же, сколько один.
3.3 Итоговая таблица
| Тип задачи | threading | multiprocessing |
|---|---|---|
| CPU-bound (чистый Python) | Не помогает (GIL) | Помогает (N процессов) |
| I/O-bound (сеть, файлы, БД) | Отлично | Избыточно |
| asyncio (I/O без потоков) | — | Не нужно |
4. Когда threading всё-таки работает
4.1 I/O-сценарии на практике
Самый частый сценарий — конкурентные HTTP-запросы. ThreadPoolExecutor из модуля concurrent.futures даёт удобный API:
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request
import time
def fetch(url):
with urllib.request.urlopen(url, timeout=5) as response:
return response.read()
urls = ["https://httpbin.org/get"] * 10
start = time.time()
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(fetch, url): url for url in urls}
for future in as_completed(futures):
data = future.result()
print(f"Fetched {len(urls)} URLs in {time.time() - start:.2f}s")
ThreadPoolExecutor управляет пулом потоков: создаёт их при необходимости, переиспользует существующие, корректно завершает при выходе из with-блока.
4.2 Имитация I/O через time.sleep
Для демонстрации работы GIL на интервью часто используют time.sleep как замену реального I/O:
import threading
import time
def worker(name, delay):
print(f"{name}: start")
time.sleep(delay) # GIL освобождается на время sleep
print(f"{name}: done after {delay}s")
start = time.time()
threads = [
threading.Thread(target=worker, args=(f"Thread-{i}", 1))
for i in range(5)
]
for t in threads: t.start()
for t in threads: t.join()
print(f"Total: {time.time() - start:.1f}s")
# Total: ~1.0s — все 5 потоков спали одновременно
time.sleep тоже освобождает GIL: поток уходит в системный вызов, интерпретатор свободен для других потоков.
4.3 Ограничения threading
Threading решает I/O-конкурентность, но не CPU-параллелизм. Есть ещё одно ограничение: shared mutable state. Когда несколько потоков изменяют один объект, нужны явные блокировки (threading.Lock), иначе возможны гонки — и это несмотря на GIL. GIL защищает внутренние структуры интерпретатора, но не делает составные операции атомарными на уровне Python-кода.
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1 # Это НЕ атомарная операция!
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # Часто не 1_000_000 — гонки при переключении GIL
counter += 1 компилируется в несколько байткод-инструкций: LOAD_GLOBAL, LOAD_CONST, BINARY_OP, STORE_GLOBAL. GIL может переключиться между любой из них.
Python-вопросы в Telegram
Ежедневные разборы GIL, asyncio и паттернов.
5. Как обойти GIL: три реальных подхода
5.1 multiprocessing: отдельные процессы
Самый надёжный способ обойти GIL для CPU-задач — multiprocessing. Каждый процесс — отдельный экземпляр Python-интерпретатора со своим GIL. Никакого разделения глобальной блокировки нет в принципе.
from multiprocessing import Pool
import time
def cpu_task(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
data = [5_000_000] * 4
# Последовательно
start = time.time()
results = [cpu_task(n) for n in data]
print(f"Sequential: {time.time() - start:.2f}s")
# Параллельно с 4 процессами
start = time.time()
with Pool(processes=4) as pool:
results = pool.map(cpu_task, data)
print(f"Multiprocessing: {time.time() - start:.2f}s")
# Реальное ускорение в ~4x на 4-ядерном CPU
ProcessPoolExecutor из concurrent.futures даёт аналогичный функционал с более удобным API:
from concurrent.futures import ProcessPoolExecutor
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_task, data))
Накладные расходы. Fork процесса дороже запуска потока: нужно создать новый адресный пространство, скопировать (или COW) память. Передача данных между процессами требует сериализации через pickle. Это значит: для задач с большими объёмами данных между процессами pickle может нивелировать выигрыш от параллелизма.
Правило: multiprocessing оправдан, когда задача на процесс занимает хотя бы десятки миллисекунд, а передаваемые данные небольшие.
5.2 C-расширения: явное освобождение GIL
Второй подход — использовать библиотеки, написанные на C, которые явно освобождают GIL на тяжёлых операциях. NumPy — главный пример.
Когда NumPy выполняет векторизованную операцию (скалярное произведение, матричное умножение, np.sort), он вызывает Py_BEGIN_ALLOW_THREADS перед вычислением и Py_END_ALLOW_THREADS после. Всё время тяжёлой C-операции GIL свободен, и другие потоки могут выполнять Python-код.
import numpy as np
import threading
import time
def numpy_task(size):
a = np.random.rand(size, size)
b = np.random.rand(size, size)
return np.dot(a, b) # Освобождает GIL на время matmul
def python_task(n):
return sum(i * i for i in range(n)) # Держит GIL
# numpy_task в двух потоках будет быстрее, чем последовательно,
# потому что np.dot освобождает GIL
start = time.time()
t1 = threading.Thread(target=numpy_task, args=(1000,))
t2 = threading.Thread(target=numpy_task, args=(1000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threading NumPy: {time.time() - start:.2f}s")
Cython позволяет писать C-расширения на синтаксисе, близком к Python, с явным управлением GIL через декораторы @cython.nogil. Для критически важных по производительности участков кода это стандартная практика.
5.3 Альтернативные интерпретаторы
Есть и более радикальные варианты — другие реализации Python без GIL:
Jython — Python на JVM. Нет GIL, есть настоящий параллелизм через Java-потоки. Но Jython застрял на Python 2.7 и не поддерживает большинство C-расширений (включая NumPy). На практике почти не используется.
PyPy STM (Software Transactional Memory) — экспериментальный режим PyPy с параллелизмом без GIL через STM. Проект активно не развивается.
Реальная альтернатива в экосистеме — это уже следующий раздел.
6. Free-threaded Python 3.13
6.1 PEP 703: Making the GIL Optional
В Python 3.13 появился экспериментальный режим без GIL, описанный в PEP 703. Автор — Сэм Гросс (Sam Gross), который несколько лет работал над патчем nogil для CPython.
Ключевые изменения в free-threaded Python:
ob_refcntзащищён через атомарные операции- Для часто изменяемых счётчиков используется biased reference counting
- Сборка мусора переработана под многопоточность
- Добавлены мьютексы на уровне отдельных объектов для критических операций
6.2 Как попробовать
В Python 3.13 появилась отдельная сборка интерпретатора: python3.13t (суффикс t — threaded). Установить можно через pyenv или официальные бинарники:
# pyenv
pyenv install 3.13t
pyenv local 3.13t
# Проверить режим
python3.13t -c "import sys; print(sys.flags.nogil)"
# True — GIL отключён
Программно проверить можно так:
import sys
if hasattr(sys, 'flags') and sys.flags.nogil:
print("Running in free-threaded mode")
else:
print("GIL is active")
6.3 Реальный параллелизм
В free-threaded режиме потоки действительно работают параллельно на нескольких ядрах. CPU-bound задачи с threading начинают масштабироваться:
import threading
import time
import sys
def cpu_task(n):
return sum(i * i for i in range(n))
N = 5_000_000
# В обычном Python: ~одинаково медленно
# В python3.13t: threading даёт реальное ускорение
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(N,))
t2 = threading.Thread(target=cpu_task, args=(N,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Time: {time.time() - start:.2f}s")
6.4 Компромиссы и статус
Это не бесплатный обед. Без GIL каждая операция с объектом требует атомарных инструкций или локальных мьютексов — это медленнее, чем просто инкремент integer. По бенчмаркам ранних версий, однопоточный код в free-threaded Python медленнее обычного на 5–10%.
Статус: в Python 3.13 режим помечен как экспериментальный и требует явной opt-in (отдельная сборка). В Python 3.14 работа продолжается — разработчики оптимизируют однопоточную производительность. Многие популярные библиотеки (NumPy, pandas) уже начали адаптироваться.
Главный риск: библиотеки, которые неявно полагались на GIL как на синхронизацию, начинают проявлять гонки. C-расширения, написанные без учёта многопоточности, становятся небезопасными. Переходный период будет долгим.
7. Типичные вопросы на собеседовании с ответами
Что такое GIL и зачем он нужен?
GIL — мьютекс в CPython, разрешающий только одному потоку выполнять байткод в каждый момент. Нужен для защиты ob_refcnt — счётчиков ссылок, через которые CPython управляет памятью. Без GIL одновременное изменение счётчиков из разных потоков привело бы к повреждению памяти.
GIL и потоки: можно ли ускорить CPU-bound программу потоками?
Нет. GIL гарантирует, что только один поток выполняет Python-байткод в каждый момент. Для CPU-bound задач threading не даёт параллелизма — только накладные расходы на переключение. Для CPU-bound нужен multiprocessing или C-расширения.
Почему I/O-bound задачи ускоряются с threading несмотря на GIL?
Потому что при I/O-операциях GIL явно освобождается: через Py_BEGIN_ALLOW_THREADS в C-реализации системных вызовов. Пока один поток ждёт ответа сети или диска, другой поток может выполнять Python-байткод. Потоки конкурентны по I/O, хоть и не параллельны по CPU.
Как обойти GIL для CPU-bound задач?
Три подхода. Первый — multiprocessing: каждый процесс имеет свой GIL, потоки процессов не конкурируют за общий замок. Второй — C-расширения (NumPy, Cython): они явно освобождают GIL во время тяжёлых вычислений. Третий — Python 3.13 в free-threaded режиме (python3.13t).
Чем ProcessPoolExecutor отличается от ThreadPoolExecutor?
ThreadPoolExecutor создаёт потоки в одном процессе — разделяют память и GIL, подходит для I/O-bound. ProcessPoolExecutor создаёт отдельные процессы — каждый со своим GIL и адресным пространством, подходит для CPU-bound, но требует pickle для передачи данных.
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# I/O-bound: потоки
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch_url, urls))
# CPU-bound: процессы
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_heavy, data))
Что такое check interval?
Интервал, через который интерпретатор проверяет, нужно ли передать GIL ожидающему потоку. До Python 3.2 — sys.getcheckinterval(), измерялся в байткод-тиках (100 по умолчанию). С Python 3.2 — sys.getswitchinterval(), измеряется в секундах (0.005 по умолчанию — 5 мс).
Влияет ли GIL на asyncio?
Нет. asyncio работает в одном потоке: event loop кооперативно переключает корутины при каждом await. GIL не участвует в этой схеме — один поток, один владелец GIL. asyncio даёт I/O-конкурентность без создания потоков вообще.
Почему не убирают GIL?
Три причины. Первая — обратная совместимость: тысячи C-расширений используют Python C API, предполагающий наличие GIL. Вторая — однопоточная производительность: GIL делает управление памятью проще и быстрее, чем fine-grained locking. Третья — сложность реализации: правильный многопоточный сборщик мусора — нетривиальная задача. Python 3.13 начал решать это в free-threaded режиме.
NumPy и GIL: как они связаны?
NumPy написан на C и явно управляет GIL: перед тяжёлой операцией вызывает Py_BEGIN_ALLOW_THREADS, освобождая GIL, после — Py_END_ALLOW_THREADS, возвращая его. Это позволяет двум потокам одновременно выполнять матричные операции NumPy — у обоих есть C-вычисления без GIL.
Что такое free-threaded Python?
Python 3.13 в режиме без GIL — отдельная сборка python3.13t. Реализует PEP 703: атомарный подсчёт ссылок, переработанный GC. Позволяет потокам работать действительно параллельно. Статус: экспериментальный в 3.13, однопоточный код медленнее на ~5–10%.
Что происходит с GIL при вызове C-расширения?
Зависит от расширения. C-функция, вызванная из Python, изначально держит GIL. Если операция долгая (I/O, вычисления, системный вызов), хорошо написанное расширение явно освобождает GIL через Py_BEGIN_ALLOW_THREADS. Плохо написанное расширение держит GIL всё время — блокируя другие потоки.
Как написать параллельную обработку данных на Python?
Зависит от природы задачи. Для I/O-bound — ThreadPoolExecutor или asyncio. Для CPU-bound — ProcessPoolExecutor с Pool.map. Для вычислительно тяжёлых задач с массивами данных — NumPy или Cython с явным освобождением GIL. Выбор инструмента начинается с вопроса: «Где тратится время — в ожидании I/O или в вычислениях?»
8. Итоги: выбор инструмента
GIL — не приговор многопоточности в Python. Это конкретное ограничение с конкретными обходными путями. Правильное решение зависит от характера задачи:
| Задача | Лучший инструмент | Почему |
|---|---|---|
| CPU-bound (чистый Python) | ProcessPoolExecutor | Отдельные процессы, нет общего GIL |
| CPU-bound (числа, массивы) | NumPy + threading | NumPy освобождает GIL для C-операций |
| I/O-bound (сеть, файлы) | ThreadPoolExecutor | GIL освобождается на I/O, потоки конкурентны |
| I/O-bound (высокая нагрузка) | asyncio | Event loop без потоков, минимальный overhead |
| CPU + I/O (смешанное) | asyncio + ProcessPoolExecutor | Offload CPU в процессы, I/O через event loop |
GIL не делает Python плохим языком для конкурентности — он делает его языком, где нужно правильно выбирать инструменты. asyncio прекрасно масштабирует тысячи соединений в одном потоке. multiprocessing даёт настоящий параллелизм для вычислений. NumPy обходит GIL для линейной алгебры. И всё это — без знания деталей реализации GIL в повседневной работе.
Но понять механику GIL важно не для того, чтобы писать лучший код прямо сейчас, а для того, чтобы правильно диагностировать проблемы: «Почему мой многопоточный код не ускоряется?», «Откуда гонки, ведь GIL должен защищать?», «Почему NumPy в потоках работает, а моя функция нет?»
Полный разбор других тем Python-собеседований — в статье Python: 50 вопросов на собеседовании junior–middle.
Потренируйся на реальных вопросах
50 вопросов по Python с разбором — GIL, asyncio, декораторы, память.
Автор
Lexicon Team
Читайте также
backend
Python: 50 вопросов на собеседовании junior–middle
Разбор 50 реальных вопросов по Python для junior и middle: типы данных, ООП, GIL и память, asyncio, декораторы и структура проекта. Примеры кода и объяснения.
backend
Threading vs multiprocessing в Python: что выбрать и почему
Глубокий разбор threading и multiprocessing в Python: GIL, CPU-bound vs I/O-bound, concurrent.futures, синхронизация, IPC и реальные примеры кода для собеседования.
backend
Управление памятью в Python: reference counting, циклический GC и утечки
Глубокий разбор управления памятью в CPython: PyObject, reference counting, циклический GC, weakref, __slots__, pymalloc и диагностика утечек с tracemalloc.