← Назад к вопросам

Как потоки взаимодействуют с 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 (сеть, БД)threadingGIL отпущен, простой
CPU-boundmultiprocessingБез GIL, настоящий параллелизм
I/O + простотаasyncioБез GIL, быстро, scalable
Ожиданиеqueue.QueueБезопасная передача между потоками
Критичный кодCython/NumPyОбход GIL

Чеклист понимания GIL

  1. GIL блокирует только Python код — C расширения освобождают GIL
  2. I/O операции отпускают GIL — потоки полезны для I/O
  3. CPU-bound код держит GIL — используй multiprocessing
  4. GIL НЕ защищает от race condition — используй Lock
  5. asyncio лучше потоков для I/O — без GIL, более масштабируемо
  6. Один поток = нет overhead — нет переключения контекста
  7. Много потоков = контекст switching — может быть медленнее

Золотое правило

GIL — это не враг, это реальность CPython. Для I/O-bound задач используй потоки (GIL отпущен). Для CPU-bound — используй multiprocessing или asyncio. Ничего не решает как понимание GIL и выбор правильного инструмента.