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

Что такое GIL (Global Interpreter Lock) и как он влияет на многопоточность?

2.0 Middle🔥 141 комментариев
#DevOps и инфраструктура#Django

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

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

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

Global Interpreter Lock (GIL) и многопоточность в Python

GIL — это одна из самых часто неправильно понимаемых концепций в Python. Давайте разберёмся, почему он существует, как он работает и как его обойти.

1. Что такое GIL?

GIL (Global Interpreter Lock) — это мьютекс (взаимное исключение), который защищает доступ к объектам Python в интерпретаторе CPython.

Основной принцип: только ОДИН поток может выполнять код Python одновременно, даже если у вас есть многоядерный процессор.

import threading
import time

def compute_expensive():
    """Вычисление без операций ввода-вывода."""
    total = 0
    for i in range(50_000_000):
        total += i
    return total

start = time.time()

# Вариант 1: Однопоточное выполнение
compute_expensive()
compute_expensive()
sequential_time = time.time() - start
print(f"Последовательное: {sequential_time:.2f}с")

start = time.time()

# Вариант 2: Многопоточное выполнение (МЕДЛЕННЕЕ!)
thread1 = threading.Thread(target=compute_expensive)
thread2 = threading.Thread(target=compute_expensive)

thread1.start()
thread2.start()

thread1.join()
thread2.join()
multi_thread_time = time.time() - start
print(f"Многопоточное: {multi_thread_time:.2f}с")

# Результат:
# Последовательное: 3.5с
# Многопоточное: 5.2с  <- МЕДЛЕННЕЕ из-за GIL!

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

Управление памятью в CPython:

# Каждый объект имеет reference count
class MyObject:
    pass

obj = MyObject()  # Reference count = 1
obj_ref = obj     # Reference count = 2

# Когда счётчик = 0, объект удаляется через garbage collection

Реализация счётчика ссылок потокобезопасна благодаря GIL:

Без GIL:                        С GIL:
┌─────────────┐               ┌─────────────┐
│   Объект    │               │   Объект    │
│ ref_count=1 │               │ ref_count=1 │
└─────────────┘               └─────────────┘
      │                             │
   Поток 1 ──┐  ┌─ Поток 2       Поток 1 ──┐ GIL ┌─ Поток 2
             ├──┤ (race condition!)         ├──────┤
             └──┘                           │ Lock │
                                            └──────┘
Проблема: оба потока                   Решение: один поток
могут изменить счётчик              одновременно получает доступ
одновременно!

Поэтому GIL нужен:

  • Безопасное управление памятью
  • Упрощение реализации
  • Простота использования C расширений

3. Визуализация работы GIL

import threading
import time

def task_with_io():
    """Задача с операциями ввода-вывода."""
    print(f"Поток {threading.current_thread().name} начал")
    time.sleep(1)  # I/O операция (GIL ОТПУСКАЕТСЯ!)
    print(f"Поток {threading.current_thread().name} завершил")

def task_cpu_bound():
    """CPU-bound задача."""
    print(f"Поток {threading.current_thread().name} начал вычисления")
    total = sum(i**2 for i in range(100_000_000))  # GIL НЕ ОТПУСКАЕТСЯ
    print(f"Поток {threading.current_thread().name} завершил вычисления")

# Тест I/O-bound (потоки работают ПАРАЛЛЕЛЬНО)
print("=== I/O-bound (потоки паузируются) ===")
start = time.time()

thread1 = threading.Thread(target=task_with_io, name="T1")
thread2 = threading.Thread(target=task_with_io, name="T2")

thread1.start()
thread2.start()
thread1.join()
thread2.join()

print(f"Время: {time.time() - start:.1f}с (оптимально ~1с)\n")

# Тест CPU-bound (потоки работают ПОСЛЕДОВАТЕЛЬНО)
print("=== CPU-bound (вычисления) ===")
start = time.time()

thread3 = threading.Thread(target=task_cpu_bound, name="T3")
thread4 = threading.Thread(target=task_cpu_bound, name="T4")

thread3.start()
thread4.start()
thread3.join()
thread4.join()

print(f"Время: {time.time() - start:.1f}с (медленнее, чем параллельно)")

Вывод:

=== I/O-bound ===
Поток T1 начал
Поток T2 начал
Поток T1 завершил
Поток T2 завершил
Время: 1.0с  ✓ Параллельно!

=== CPU-bound ===
Поток T3 начал вычисления
Поток T3 завершил вычисления
Поток T4 начал вычисления
Поток T4 завершил вычисления
Время: 6.5с  ✗ Последовательно!

4. Когда GIL НЕ мешает

I/O-bound операции (GIL отпускается):

  • Сетевые запросы
  • Чтение/запись файлов
  • Запросы к БД
  • HTTP вызовы
import threading
import asyncio
import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_url(url):
    """Загрузка страницы (GIL отпускается!)."""
    response = requests.get(url)
    return len(response.text)

urls = [
    "https://example.com",
    "https://google.com",
    "https://github.com"
]

# Многопоточность ЭФФЕКТИВНА для I/O
with ThreadPoolExecutor(max_workers=3) as executor:
    results = list(executor.map(fetch_url, urls))

print(f"Загружено {sum(results)} байт")

5. Когда GIL МЕШАЕТ

CPU-bound операции (GIL не отпускается):

import threading
import time
from multiprocessing import Process, cpu_count

def cpu_intensive():
    """Чистые вычисления без I/O."""
    return sum(i**2 for i in range(100_000_000))

print(f"Количество ядер: {cpu_count()}")

# Вариант 1: Threading (ПЛОХО)
print("\n=== Threading (с GIL) ===")
start = time.time()

threads = [
    threading.Thread(target=cpu_intensive),
    threading.Thread(target=cpu_intensive)
]

for t in threads:
    t.start()
for t in threads:
    t.join()

threading_time = time.time() - start
print(f"Время: {threading_time:.1f}с")

# Вариант 2: Multiprocessing (ХОРОШО)
print("\n=== Multiprocessing (без GIL) ===")
start = time.time()

processes = [
    Process(target=cpu_intensive),
    Process(target=cpu_intensive)
]

for p in processes:
    p.start()
for p in processes:
    p.join()

multiprocessing_time = time.time() - start
print(f"Время: {multiprocessing_time:.1f}с")

print(f"\nMultiprocessing в {threading_time / multiprocessing_time:.1f}х быстрее!")

Результат:

Количество ядер: 4

=== Threading ===
Время: 5.2с  (выполнение последовательное)

=== Multiprocessing ===
Время: 1.6с  (выполнение параллельное на разных ядрах)

Multiprocessing в 3.3х быстрее!

6. Решения и альтернативы

Решение 1: Использование multiprocessing

from multiprocessing import Pool

def heavy_computation(n):
    return sum(i**2 for i in range(n))

if __name__ == '__main__':
    # Создаём процесс для каждого ядра
    with Pool(processes=4) as pool:
        results = pool.map(heavy_computation, [100_000_000] * 4)
    
    print(f"Результаты: {results}")

Решение 2: Использование asyncio для I/O

import asyncio
import aiohttp

async def fetch_many_urls(urls):
    """Загрузка множества URL без потоков!"""
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
    return [await r.text() for r in responses]

urls = ["https://example.com"] * 10
results = asyncio.run(fetch_many_urls(urls))
print(f"Загружено {len(results)} страниц")

Решение 3: C расширения (Cython, ctypes)

# Cython код (heavy_computation.pyx)
# cdef int heavy_computation_c(int n):
#     return sum(i**2 for i in range(n))

# При вызове из C — GIL отпускается!
from heavy_computation import heavy_computation_c

result = heavy_computation_c(100_000_000)  # Без блокировки GIL

Решение 4: PyPy, Jython, IronPython (альтернативные интерпретаторы)

CPython       → GIL (нужен для управления памятью ref-counting)
PyPy          → GIL (но JIT оптимизирует лучше)
Jython        → БЕЗ GIL (использует Java threads)
IronPython    → БЕЗ GIL (использует .NET threads)

7. GIL в Python 3.13+

Python 3.13 вводит опциональное отключение GIL:

# Запуск без GIL
# python -X gil=0 script.py

# Или в коде
import sys
sys.flags.optimize  # Можно проверить режим выполнения

Сравнительная таблица решений

ПодходЛучше дляПлюсыМинусы
ThreadingI/OПростой API, shared memoryGIL для CPU-bound
MultiprocessingCPUИстинный parallelismOverhead, IPC сложнее
asyncioI/OЭффективный, не требует потоковИзменение кода
CythonCPUВысокая скоростьТребует компиляции
Jython/IronPythonCPUБЕЗ GILНесовместимые библиотеки

Практические рекомендации

# Диагностика: является ли задача I/O или CPU bound?

# CPU-bound: используй multiprocessing
from multiprocessing import Pool

def cpu_task():
    return sum(i**2 for i in range(100_000_000))

with Pool() as pool:
    results = pool.map(cpu_task, range(4))

# I/O-bound: используй asyncio или threading
import asyncio

async def io_task(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

asyncio.run(io_task("https://example.com"))

Вывод

GIL НЕ проблема для:

  • I/O-bound приложений (веб-серверы, API)
  • Асинхронного кода (asyncio)
  • Задач с использованием C расширений

GIL ЯВЛЯЕТСЯ проблемой для:

  • CPU-bound многопоточного кода
  • Вычислений на всех ядрах
  • Численных алгоритмов в чистом Python

Выбирайте правильный инструмент для задачи: threading для I/O, multiprocessing для CPU.