← Назад к вопросам
Как потоки взаимодействуют с GIL в Python?
2.8 Senior🔥 121 комментариев
#Python Core#Асинхронность и многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как потоки взаимодействуют с GIL в Python
GIL (Global Interpreter Lock) — это одна из самых непонятных и важных деталей Python. За 10+ лет я видел столько ошибок из-за неправильного понимания GIL.
1. Что такое GIL?
GIL — это мьютекс, который защищает доступ к объектам в CPython. В один момент времени только ОДИН поток может выполнять Python код.
import threading
import time
def cpu_bound_task():
"""CPU-интенсивная задача"""
total = 0
for i in range(50000000):
total += i
return total
# ❌ НЕПРАВИЛЬНО: потокам не помогает GIL
start = time.time()
threads = [
threading.Thread(target=cpu_bound_task),
threading.Thread(target=cpu_bound_task),
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Время: {time.time() - start:.2f}s") # ~4 сек (как за один поток)
# GIL не позволяет параллелизму, потоки выполняются поочередно
2. GIL НЕ защищает от проблем
Многие думают, что GIL решает race condition — ОШИБКА:
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1 # counter + 1 + присвоение = 3 операции!
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Counter: {counter}") # Ожидаем 1000000, получаем ~700000
# GIL не защищает от race condition в Python коде
# Правильно: используй Lock
lock = threading.Lock()
counter = 0
def increment_safe():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = [threading.Thread(target=increment_safe) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Counter: {counter}") # 1000000 — правильно
3. Когда GIL отпускается?
GIL отпускается в несколько случаев:
import threading
import time
import requests # I/O операция
# Случай 1: Блокирующие I/O операции
def io_bound_task(url):
# GIL отпускается при network.request()
response = requests.get(url) # GIL ОТПУЩЕН здесь
return len(response.content)
# Потоки работают параллельно!
threads = [
threading.Thread(target=io_bound_task, args=("https://example.com",))
for _ in range(10)
]
start = time.time()
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Время: {time.time() - start:.2f}s") # ~2 сек (не ~20 сек)
# Случай 2: sys.setswitchinterval() — интервал переключения
import sys
sys.setswitchinterval(0.001) # Переключаться чаще (по умолчанию 0.005)
# Но это не сделает CPU-bound код быстрее, только медленнее
4. GIL для разных операций
import threading
import time
# ✅ I/O-bound: потоки ПОМОГАЮТ
def io_operation():
import requests
for _ in range(5):
requests.get("https://httpbin.org/delay/1") # GIL отпущен
start = time.time()
threads = [threading.Thread(target=io_operation) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"I/O-bound: {time.time() - start:.2f}s") # ~5 сек (не ~25 сек)
# ❌ CPU-bound: потоки НЕ помогают
def cpu_operation():
total = 0
for i in range(50000000):
total += i
return total
start = time.time()
threads = [threading.Thread(target=cpu_operation) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"CPU-bound (потоки): {time.time() - start:.2f}s") # ~10 сек
# Для CPU-bound используй multiprocessing
import multiprocessing
if __name__ == '__main__':
start = time.time()
with multiprocessing.Pool(5) as pool:
pool.map(cpu_operation, range(5))
print(f"CPU-bound (процессы): {time.time() - start:.2f}s") # ~3 сек
5. Измерение влияния GIL
import threading
import time
def cpu_bound(n):
result = 0
for i in range(n):
result += i ** 2
return result
# Один поток
start = time.time()
cpu_bound(50000000)
time_single = time.time() - start
print(f"Один поток: {time_single:.2f}s")
# Два потока (GIL активен)
start = time.time()
threads = [
threading.Thread(target=cpu_bound, args=(50000000,)),
threading.Thread(target=cpu_bound, args=(50000000,)),
]
for t in threads:
t.start()
for t in threads:
t.join()
time_two_threads = time.time() - start
print(f"Два потока с GIL: {time_two_threads:.2f}s") # ~2x медленнее!
# Два процесса (без GIL)
import multiprocessing
if __name__ == '__main__':
start = time.time()
with multiprocessing.Pool(2) as pool:
pool.map(cpu_bound, [50000000] * 2)
time_two_processes = time.time() - start
print(f"Два процесса без GIL: {time_two_processes:.2f}s") # ~2x медленнее нормально
6. asyncio — альтернатива потокам
asyncio НЕ использует GIL, потому что работает в одном потоке:
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return len(await response.text())
async def main():
async with aiohttp.ClientSession() as session:
tasks = [
fetch_url(session, "https://httpbin.org/delay/1")
for _ in range(10)
]
results = await asyncio.gather(*tasks)
return sum(results)
import time
start = time.time()
result = asyncio.run(main())
print(f"asyncio время: {time.time() - start:.2f}s") # ~1-2 сек (очень быстро!)
7. Обход GIL: C расширения
Для критичного кода можно использовать C расширения:
# ❌ Python код с GIL
def slow_sum(n):
total = 0
for i in range(n):
total += i
return total
# ✅ NumPy (написан на C, GIL отпущен)
import numpy as np
def fast_sum(n):
return np.sum(np.arange(n))
# fast_sum в 100x быстрее и не держит GIL
8. Python 3.13+: Без GIL (Free-threading)
Уже в Python 3.13 можно отключить GIL:
# Запуск Python без GIL
python -X gil=0 script.py
До Python 3.13:
import sys
# Проверка, включен ли GIL
try:
sys._getframe()
print("GIL включен (обычный Python)")
except:
print("GIL отключен (free-threaded Python)")
9. Практический пример: правильное использование потоков
import threading
import requests
import queue
# ✅ ПРАВИЛЬНО: потоки для I/O
class DownloadWorker(threading.Thread):
def __init__(self, queue):
super().__init__()
self.queue = queue
self.daemon = True # Завершиться с основной программой
def run(self):
while True:
url = self.queue.get()
if url is None:
break
try:
response = requests.get(url) # GIL отпущен!
print(f"Загружено {url}: {len(response.content)} bytes")
except Exception as e:
print(f"Ошибка {url}: {e}")
finally:
self.queue.task_done()
q = queue.Queue()
# Создаём 5 рабочих потоков
for _ in range(5):
worker = DownloadWorker(q)
worker.start()
# Добавляем задачи
urls = ["https://example.com"] * 20
for url in urls:
q.put(url)
# Ждём завершения
q.join()
# Останавливаем рабочих
for _ in range(5):
q.put(None)
10. Таблица: выбор инструмента
| Задача | Инструмент | Причина |
|---|---|---|
| I/O-bound (сеть, БД) | threading | GIL отпущен, простой |
| CPU-bound | multiprocessing | Без GIL, настоящий параллелизм |
| I/O + простота | asyncio | Без GIL, быстро, scalable |
| Ожидание | queue.Queue | Безопасная передача между потоками |
| Критичный код | Cython/NumPy | Обход GIL |
Чеклист понимания GIL
- GIL блокирует только Python код — C расширения освобождают GIL
- I/O операции отпускают GIL — потоки полезны для I/O
- CPU-bound код держит GIL — используй multiprocessing
- GIL НЕ защищает от race condition — используй Lock
- asyncio лучше потоков для I/O — без GIL, более масштабируемо
- Один поток = нет overhead — нет переключения контекста
- Много потоков = контекст switching — может быть медленнее
Золотое правило
GIL — это не враг, это реальность CPython. Для I/O-bound задач используй потоки (GIL отпущен). Для CPU-bound — используй multiprocessing или asyncio. Ничего не решает как понимание GIL и выбор правильного инструмента.