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

Как связаны GIL, многопоточность и вытесняющая многозадачность?

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

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

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

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

GIL, многопоточность и вытесняющая многозадачность

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

Что такое GIL?

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

# ПЛОХО: многопоточность не поможет для CPU-bound задач
import threading
import time

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

# Однопоточное выполнение
start = time.time()
cpu_bound_task()
print(f"Single thread: {time.time() - start:.2f}s")  # ~5 сек

# Двухпоточное выполнение
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"Two threads: {time.time() - start:.2f}s")  # ~10 сек (медленнее!)
# GIL не позволяет параллельное выполнение

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

  1. Упрощение реализации — не нужны блокировки для каждого объекта
  2. Безопасность памяти — автоматическое управление памятью (garbage collection)
  3. Совместимость с C расширениями — многие C библиотеки не потокобезопасны
// Внутри CPython (упрощённо)
struct PyObject {
    int refcount;  // Счётчик ссылок
    PyTypeObject *type;
};

// GIL защищает все операции с refcount
// Без GIL пришлось бы использовать сложные атомарные операции

Вытесняющая многозадачность (Preemptive Multitasking)

Python использует вытесняющую многозадачность: операционная система переключает потоки, а не сам код. Это происходит каждые ~5 миллисекунд (настраивается через sys.setswitchinterval()).

import threading
import sys

# Интервал переключения потоков (по умолчанию 5ms)
print(f"Switch interval: {sys.getswitchinterval()}s")

# Изменить интервал (внимание: частые переключения = больше overhead)
sys.setswitchinterval(0.001)  # 1ms

def task(name):
    for i in range(3):
        print(f"{name}: {i}")

t1 = threading.Thread(target=task, args=("Thread-1",))
t2 = threading.Thread(target=task, args=("Thread-2",))

t1.start()
t2.start()
t1.join()
t2.join()

# Вывод показывает перемешивание выполнения (вытесняющая многозадачность)
# Thread-1: 0
# Thread-2: 0
# Thread-1: 1
# Thread-2: 1
# ...

Жизненный цикл GIL

Поток 1                     GIL              Поток 2
  |
  | (захватывает GIL)
  |-----------> [GIL] <----|
  |                        |
  | (выполняет Python код) | (ждёт GIL)
  |                        |
  | ~5ms прошло            |
  |                        |
  | (отпускает GIL)        |
  |-----------> [GIL] <----|
  |                        |
  | (ждёт GIL)             | (выполняет Python код)
  |                        |
  |                        | ~5ms прошло
  |                        |
  |                        | (отпускает GIL)

I/O-bound vs CPU-bound задачи

I/O-bound (многопоточность работает)

import threading
import requests
import time

def fetch_data(url):
    response = requests.get(url)
    return len(response.content)

# Однопоточное
start = time.time()
for url in [url1, url2, url3]:
    fetch_data(url)
print(f"Single thread: {time.time() - start:.2f}s")  # ~3 сек (3 запроса * 1 сек)

# Многопоточное
start = time.time()
threads = []
for url in [url1, url2, url3]:
    t = threading.Thread(target=fetch_data, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Three threads: {time.time() - start:.2f}s")  # ~1 сек (параллельно!)

# Многопоточность помогает! Потому что:
# - Когда поток ждёт I/O, GIL отпускается
# - Другие потоки могут работать одновременно

Почему это работает: Когда поток выполняет I/O операцию (например, requests.get()), он вызывает системный вызов. На время этого вызова GIL отпускается, позволяя другим потокам выполняться.

CPU-bound (многопоточность не помогает)

import threading
import time

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

n = 100_000_000

# Однопоточное
start = time.time()
cpu_task(n)
print(f"Single thread: {time.time() - start:.2f}s")  # 5 сек

# Двухпоточное
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(n,))
t2 = threading.Thread(target=cpu_task, args=(n,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Two threads: {time.time() - start:.2f}s")  # 10 сек! (медленнее из-за overhead)

# GIL не отпускается при CPU вычислениях
# Потоки просто конкурируют за GIL
# Результат: последовательное выполнение + overhead переключения

Решение 1: multiprocessing (для CPU-bound)

from multiprocessing import Pool
import time

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

n = 100_000_000

# Многопроцессность (разные процессы = разные интерпретаторы = разные GIL)
start = time.time()
with Pool(processes=2) as pool:
    results = pool.map(cpu_task, [n, n])
print(f"Two processes: {time.time() - start:.2f}s")  # ~5 сек (параллельно!)

# Каждый процесс имеет свой GIL
# Процесс 1 выполняет на CPU 0
# Процесс 2 выполняет на CPU 1 одновременно

Решение 2: asyncio (для I/O-bound)

import asyncio
import aiohttp
import time

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in [url1, url2, url3]]
        results = await asyncio.gather(*tasks)
    return results

# asyncio — это кооперативная многозадачность (не вытесняющая)
# Только один поток выполняется в один момент
# Но переключение происходит явно (await), не прерывая GIL

start = time.time()
asyncio.run(main())
print(f"Asyncio: {time.time() - start:.2f}s")  # ~1 сек (параллельно)

Решение 3: Cython или ctypes (отпустить GIL)

# Cython пример
# cython_module.pyx
cdef extern from "math.h":
    double sqrt(double x) nogil

def compute_sqrt(double n):
    cdef double result
    with nogil:  # Выпустить GIL на время вычисления
        result = sqrt(n)
    return result

Инструменты для профилирования GIL

import threading
import time

def monitor_gil():
    """Показать, как часто потоки переключаются"""
    import sys
    
    switch_count = 0
    last_frame = None
    
    def trace_calls(frame, event, arg):
        nonlocal switch_count, last_frame
        if event == 'return':
            if last_frame is not None and last_frame[0] != frame[0]:
                switch_count += 1
        last_frame = frame
        return trace_calls
    
    sys.settrace(trace_calls)
    time.sleep(1)
    sys.settrace(None)
    
    print(f"GIL switches in 1 second: {switch_count}")

monitor_gil()

Таблица: Когда использовать что

ЗадачаИнструментПочему
I/O-bound (сеть, файлы)threadingGIL отпускается при I/O, параллелизм работает
I/O-bound асинхронноеasyncioБолее эффективно, меньше overhead
CPU-boundmultiprocessingРазные процессы, разные GIL
CPU-bound (NumPy, SciPy)threadingЭти библиотеки сами отпускают GIL
СмешанноекомбинацияНапример, asyncio + multiprocessing

Будущее: PEP 703 (No-GIL Python)

# Python 3.13+ возможно будет без GIL
# Это позволит true параллелизм даже для CPU-bound

# Но пока используй
# - multiprocessing для CPU-bound
# - threading/asyncio для I/O-bound

Краткое резюме

  1. GIL — это мьютекс, позволяющий только одному потоку выполнять Python код
  2. Вытесняющая многозадачность — ОС переключает потоки каждые ~5ms
  3. I/O-bound задачи: GIL отпускается, threading работает эффективно
  4. CPU-bound задачи: GIL не отпускается, используй multiprocessing
  5. asyncio — кооперативная многозадачность, часто лучше threading для I/O

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

Как связаны GIL, многопоточность и вытесняющая многозадачность? | PrepBro