← Назад к вопросам
Как сделать параллельное исполнение потоков при наличии 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:
- CPU-bound: используй
multiprocessingилиnumba - I/O-bound: используй
threadingилиasyncio - Комбинировано: используй оба подхода
- Python 3.13+: экспериментальный флаг
--disable-gil
Выбор правильного инструмента — ключ к производительности в Python.