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

Как сделать параллельное исполнение потоков при наличии GIL в Python?

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

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

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

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

Параллельное исполнение потоков при GIL

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

1. Что такое GIL

import threading
import time

def cpu_bound(n):
    """CPU-bound операция"""
    total = 0
    for i in range(n):
        total += i
    return total

# Без потоков — работает в одном потоке
start = time.time()
cpu_bound(100_000_000)
cpu_bound(100_000_000)
print(f"Sequential: {time.time() - start:.2f}s")  # ~4-5 сек

# С потоками — практически то же самое!
t1 = threading.Thread(target=cpu_bound, args=(100_000_000,))
t2 = threading.Thread(target=cpu_bound, args=(100_000_000,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Threaded: {time.time() - start:.2f}s")  # Тоже ~4-5 сек!

# GIL не дает параллелизм для CPU-bound операций

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

I/O-bound операции (сетевые запросы, файлы):

import threading
import time
import requests

def fetch_url(url):
    """I/O-bound: ждет ответа от сервера"""
    response = requests.get(url)
    print(f"Fetched: {url} ({len(response.content)} bytes)")

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

# Без потоков
start = time.time()
for url in urls:
    fetch_url(url)
print(f"Sequential: {time.time() - start:.2f}s")  # ~9 сек (3 сек каждый)

# С потоками — намного быстрее!
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
start = time.time()
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Threaded: {time.time() - start:.2f}s")  # ~3 сек (параллельно!)

# GIL отпускается во время I/O ожидания

3. Решение 1: Multiprocessing (для CPU-bound)

Каждый процесс имеет свой GIL:

from multiprocessing import Process
import time

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

if __name__ == '__main__':
    start = time.time()
    
    p1 = Process(target=cpu_bound, args=(100_000_000,))
    p2 = Process(target=cpu_bound, args=(100_000_000,))
    
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    
    print(f"Multiprocessing: {time.time() - start:.2f}s")  # ~2.5 сек (истинный параллелизм!)

С ProcessPoolExecutor:

from concurrent.futures import ProcessPoolExecutor

def cpu_task(n):
    return sum(range(n))

with ProcessPoolExecutor(max_workers=4) as executor:
    results = executor.map(cpu_task, [10_000_000] * 4)
    list(results)

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

Асинхронность без потоков:

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    """Асинхронный запрос"""
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        'https://example.com',
        'https://google.com',
        'https://github.com'
    ]
    
    async with aiohttp.ClientSession() as session:
        start = time.time()
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        print(f"Asyncio: {time.time() - start:.2f}s")  # ~3 сек (быстро и без потоков!)

if __name__ == '__main__':
    asyncio.run(main())

5. Решение 3: Cython / Numba (без GIL)

Cython с cdef функциями (типизированный Python):

# functions.pyx
# cython: language_level=3, boundscheck=False, wraparound=False

cdef long cpu_bound_cython(long n):
    cdef long i, total = 0
    for i in range(n):
        total += i
    return total

def cpu_bound(n):
    return cpu_bound_cython(n)

Numba для быстрого кода:

from numba import jit
import threading
import time

@jit(nopython=True)
def cpu_bound_numba(n):
    total = 0
    for i in range(n):
        total += i
    return total

# Теперь GIL не нужен! Код скомпилирован в машинный код
t1 = threading.Thread(target=cpu_bound_numba, args=(100_000_000,))
t2 = threading.Thread(target=cpu_bound_numba, args=(100_000_000,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Numba + Threading: {time.time() - start:.2f}s")  # ~2 сек (параллельно!)

6. Решение 4: Threading для I/O (правильно)

import threading
from queue import Queue

class Worker(threading.Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue
        self.daemon = True
    
    def run(self):
        while True:
            task = self.queue.get()
            if task is None:
                break
            # I/O операция — GIL отпускается
            result = fetch_data(task)
            print(f"Processed: {result}")
            self.queue.task_done()

queue = Queue()
workers = [Worker(queue) for _ in range(4)]
for w in workers:
    w.start()

# Добавляем задачи
for item in items:
    queue.put(item)

queue.join()
for _ in workers:
    queue.put(None)

7. Python 3.13+ без GIL

# Python 3.13 с флагом --disable-gil
# GIL можно отключить при компиляции Python
# Но это все еще экспериментальное

import threading
import time

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

if __name__ == '__main__':
    # В Python 3.13 с --disable-gil это будет параллельно!
    t1 = threading.Thread(target=cpu_bound, args=(100_000_000,))
    t2 = threading.Thread(target=cpu_bound, args=(100_000_000,))
    start = time.time()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(f"No GIL: {time.time() - start:.2f}s")  # ~2 сек

8. Таблица решений

Тип задачи  | Проблема    | Решение        | Скорость
------------|-------------|----------------|----------
CPU-bound   | GIL         | Multiprocessing| ✅ Быстро
I/O-bound   | Ожидание    | Threading      | ✅ Быстро
I/O-bound   | Ожидание    | Asyncio        | ✅✅ Очень быстро
CPU-bound   | GIL         | Numba/Cython   | ✅✅ Очень быстро
Mix         | Оба         | Hybrid         | ✅ Зависит

9. Практический пример: микс

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time

def cpu_task(n):
    return sum(range(n))

def io_task(url):
    import requests
    return requests.get(url).status_code

with ProcessPoolExecutor(max_workers=2) as cpu_executor:
    with ThreadPoolExecutor(max_workers=4) as io_executor:
        # CPU-bound в процессах
        cpu_results = cpu_executor.map(cpu_task, [10_000_000] * 2)
        
        # I/O-bound в потоках
        io_results = io_executor.map(io_task, [
            'https://example.com',
            'https://google.com'
        ])
        
        print(list(cpu_results))
        print(list(io_results))

Итог

GIL ограничивает CPU-bound операции, но НЕ мешает I/O-bound:

  1. CPU-bound: используй multiprocessing или numba
  2. I/O-bound: используй threading или asyncio
  3. Комбинировано: используй оба подхода
  4. Python 3.13+: экспериментальный флаг --disable-gil

Выбор правильного инструмента — ключ к производительности в Python.