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

Зачем GIL блокирует потоки?

3.0 Senior🔥 131 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Global Interpreter Lock (GIL) — Зачем он блокирует потоки?

GIL (Global Interpreter Lock) — это механизм в CPython (стандартной реализации Python), который позволяет только одному потоку выполнять Python код одновременно. Это звучит странно, но у этого есть веские причины.

Проблема: управление памятью в CPython

CPython использует reference counting (подсчет ссылок) для управления памятью:

import sys

obj = [1, 2, 3]
print(sys.getrefcount(obj))  # Количество ссылок на объект

obj2 = obj  # +1 ссылка
print(sys.getrefcount(obj))  # Увеличилось на 1

del obj2  # -1 ссылка
print(sys.getrefcount(obj))  # Уменьшилось на 1

# Когда refcount == 0, объект удаляется из памяти

Когда refcount становится 0, объект автоматически удаляется из памяти.

Проблема с многопоточностью:

Если два потока одновременно работают с одним объектом, может произойти race condition:

# Два потока, один объект
shared_list = [1, 2, 3]

# Поток 1 читает refcount
refcount = 5  # Прочитано из памяти

# Поток 2 удаляет последнюю ссылку
# Но Поток 1 еще использует refcount!

# Поток 1 уменьшает refcount: 5 → 4
# Но refcount должен был быть 1, и объект должен был удалиться!

# Результат: утечка памяти или use-after-free

Решение: Global Interpreter Lock

GIL — это взаимное исключение (mutex), которое гарантирует, что только один поток может выполнять Python bytecode за раз:

Поток 1              GIL (mutex)            Поток 2
   │                    │                      │
   └──→ захватить GIL ─→ │ ←─ ждет GIL ←──────┘
   │                    │
   │ выполняет Python code (refcount безопасен)
   │
   └──→ освобождает GIL ─→ │ ←─ захватывает ─→ выполняет Python code
                          │

Гарантии GIL:

  • Только один поток выполняет Python код
  • refcount операции безопасны
  • Не нужны дополнительные блокировки для каждого объекта

Пример: проблема без GIL

Представьте, что GIL отсутствует и два потока работают одновременно:

import threading

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
    
    def deposit(self, amount):
        # Без GIL это может быть прервано между чтением и записью!
        temp = self.balance  # Прочитать
        temp += amount       # Увеличить
        self.balance = temp  # Записать

account = BankAccount(100)

def thread_func():
    for _ in range(1000000):
        account.deposit(1)

# Два потока добавляют по 1 млн раз
t1 = threading.Thread(target=thread_func)
t2 = threading.Thread(target=thread_func)

t1.start()
t2.start()

t1.join()
t2.join()

# Ожидаем: 100 + 2_000_000 = 2_000_100
print(account.balance)  # Может быть например 1_234_567 (неправильно!)

Без GIL может произойти:

Поток 1                          Поток 2

temp1 = 100 (читает balance)
                                 temp2 = 100 (читает balance)
temp1 += 1 (становится 101)
                                 temp2 += 1 (становится 101)
balance = 101 (записывает)
                                 balance = 101 (записывает)

Результат: balance = 101 (вместо 102!)
Операция потеряна из-за race condition

GIL предотвращает это:

Поток 1                          Поток 2

[захватил GIL]
temp1 = 100
temp1 += 1
balance = 101
[отпустил GIL]
                                 [захватил GIL]
                                 temp2 = 101
                                 temp2 += 1
                                 balance = 102
                                 [отпустил GIL]

Результат: balance = 102 (правильно!)

Как GIL работает

CPython использует слабый GIL:

// Упрощённо
while (есть инструкции) {
    // Каждые N инструкций проверить, хочет ли другой поток GIL
    if (check_interval % инструкция == 0) {
        // Отпустить GIL на время
        // Позволить другому потоку выполниться
        // Заново захватить GIL
    }
    // Выполнить инструкцию
    выполнить_bytecode();
}

Каждые ~5 млн инструкций Python переключается контекст (примерно).

Практические последствия GIL

Многопоточность работает только для I/O:

import threading
import time

def cpu_bound_task():
    """CPU-интенсивная задача"""
    total = 0
    for i in range(100_000_000):
        total += i
    return total

def io_bound_task():
    """I/O-интенсивная задача"""
    import requests
    response = requests.get("https://api.example.com/data")
    return response.json()

# CPU-bound с потоками — БЕЗ улучшения
start = time.time()
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"2 потока (CPU): {time.time() - start:.2f}s")  # ~10 сек

# CPU-bound без потоков — БЫСТРЕЕ!
start = time.time()
cpu_bound_task()
cpu_bound_task()
print(f"Без потоков (CPU): {time.time() - start:.2f}s")  # ~10 сек (одинаково)

# I/O-bound с потоками — РАБОТАЕТ, потоки ждут друг друга
start = time.time()
t1 = threading.Thread(target=io_bound_task)
t2 = threading.Thread(target=io_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"2 потока (I/O): {time.time() - start:.2f}s")  # ~1 сек (параллельно)

# I/O-bound без потоков
start = time.time()
io_bound_task()
io_bound_task()
print(f"Без потоков (I/O): {time.time() - start:.2f}s")  # ~2 сек (последовательно)

Почему I/O работает? Когда поток ждет I/O (сеть, диск), он отпускает GIL:

# Внутри requests.get()
GIL.release()  # Отпустить
perform_http_request()  # Ждем ответ от сервера
GIL.acquire()  # Захватить обратно

Решения для CPU-bound задач

1. Multiprocessing (отдельные процессы)

import multiprocessing

def cpu_task():
    total = 0
    for i in range(100_000_000):
        total += i
    return total

if __name__ == "__main__":
    # Каждый процесс имеет свой Python интерпретатор и свой GIL
    # Они работают в параллель на разных ядрах!
    with multiprocessing.Pool(processes=2) as pool:
        results = pool.map(cpu_task, [1, 2])
    
    print(results)

2. Asyncio (асинхронное программирование)

import asyncio

async def fetch_data(url):
    # Асинхронный код, работает с одним потоком
    # но выполняет множество операций I/O одновременно
    # await отпускает управление, другие корутины работают
    result = await get_data_async(url)
    return result

async def main():
    # 10 запросов параллельно
    results = await asyncio.gather(
        *[fetch_data(f"https://api{i}.example.com") for i in range(10)]
    )
    return results

asyncio.run(main())

3. NumPy, Pandas, Cython (работают вне GIL)

import numpy as np

# NumPy операции работают на C уровне и отпускают GIL
arr = np.arange(1_000_000)
result = np.sum(arr)  # Не заблокирована GIL

# Cython код может явно отпустить GIL
# cdef void fast_function() nogil:
#     # Код здесь работает без GIL

4. Python 3.13+ (GIL можно отключить)

Создатели Python работают над тем, чтобы сделать GIL опциональным:

# Python 3.13+ может запуститься без GIL
python -X gil=0 script.py  # Экспериментальная функция

Почему GIL всё ещё существует?

  1. Совместимость — много C расширений полагаются на GIL
  2. Производительность — однопоточный код работает быстрее
  3. Простота — не нужны блокировки для каждого объекта
  4. История — удалить GIL сложно без серьёзного переписания

Чек-лист для работы с GIL

Для I/O-bound (web, базы, файлы):

  • ✅ threading работает
  • ✅ asyncio работает (лучше)
  • ✅ requests, httpx, aiohttp

Для CPU-bound (вычисления):

  • ❌ threading НЕ работает
  • ✅ multiprocessing работает
  • ✅ NumPy, Pandas (работают вне GIL)
  • ✅ asyncio НЕ работает (не подходит для CPU)

Заключение

GIL — это фундаментальная часть CPython, которая:

  • Защищает from race conditions при управлении памятью
  • Позволяет reference counting быть эффективным
  • Делает однопоточный код быстрее
  • Но блокирует многопоточность для CPU-bound задач

Понимание GIL критично для выбора правильной модели параллелизма в Python.