Как устроен GIL?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как устроен GIL (Global Interpreter Lock)
GIL — это мьютекс в CPython, который позволяет только одному потоку исполнять Python байт-код одновременно. Это классический вопрос на собеседованиях, и я объясню, как это работает и почему.
Суть проблемы
Примерно до версии 1.5, Python был однопоточным. Когда добавили многопоточность, создатели столкнулись с проблемой: управление памятью в CPython использует reference counting. Если несколько потоков одновременно изменяют счётчик ссылок объекта, это приводит к race condition и утечкам памяти.
Вместо того чтобы сделать каждый объект thread-safe (с собственным мьютексом), создали GIL — глобальный мьютекс на всю интерпретатор.
Как это работает
import threading
import time
def cpu_bound_task(n):
total = 0
for i in range(n):
total += i
return total
# С потоками GIL будет одним, поэтому медленнее
start = time.time()
thread1 = threading.Thread(target=cpu_bound_task, args=(100000000,))
thread2 = threading.Thread(target=cpu_bound_task, args=(100000000,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Threading: {time.time() - start:.2f}s")
# Без потоков быстрее, потому что нет overhead переключения контекста
start = time.time()
cpu_bound_task(100000000)
cpu_bound_task(100000000)
print(f"Sequential: {time.time() - start:.2f}s")
В примере выше многопоточный вариант часто медленнее из-за GIL и overhead управления потоками.
Когда GIL НЕ мешает
GIL отпускается при блокирующих I/O операциях:
import threading
import requests
def fetch_url(url):
response = requests.get(url) # GIL отпускается здесь
return response.status_code
# Многопоточность эффективна для I/O
threads = [
threading.Thread(target=fetch_url, args=(url,))
for url in ["https://api.com/1", "https://api.com/2"]
]
for t in threads:
t.start()
for t in threads:
t.join()
Здесь многопоточность работает эффективно, потому что потоки ждут сети, а не исполняют Python код.
Как я обхожу GIL
1. Асинхронное программирование (asyncio)
import asyncio
async def fetch(url):
# Эмулирую HTTP запрос
await asyncio.sleep(1)
return f"Response from {url}"
async def main():
tasks = [fetch(f"url{i}") for i in range(10)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
Это один поток, но he поддерживает concurrency. Я использую это в web приложениях (FastAPI, aiohttp).
2. Multiprocessing для CPU-bound задач
from multiprocessing import Pool
def cpu_bound(n):
return sum(i for i in range(n))
if __name__ == "__main__":
with Pool(4) as p:
results = p.map(cpu_bound, [100000000] * 4)
Каждый процесс имеет собственный Python интерпретатор и свой GIL. Для вычислений — оптимальный выбор.
3. Внешние библиотеки
Много библиотек (NumPy, pandas) написаны на C и отпускают GIL при работе с данными.
В Python 3.13+
Текущее разработки PEP 703 предлагает полностью убрать GIL, но это требует переписания internals CPython. Это займёт годы.
Выводы: GIL — это компромисс. Он делает CPython простым и безопасным, но ограничивает многопоточность для CPU-bound задач. Знание его особенностей — критическое для правильного выбора модели concurrency в Python.