Почему асинхронность не подходит для CPU bound задач?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему асинхронность не подходит для CPU-bound задач
Асинхронность (async/await) в Python оптимальна для I/O-bound задач, но не даёт преимуществ для CPU-bound операций из-за особенности Python — Global Interpreter Lock (GIL).
1. Global Interpreter Lock (GIL)
GIL — это мьютекс (блокировка), которая позволяет только одному потоку выполнять Python код одновременно:
import threading
import time
def cpu_bound_task(n):
"""Вычисление факториала — CPU bound"""
total = 0
for i in range(n):
total += i * i
return total
# Синхронно (один поток)
start = time.time()
result1 = cpu_bound_task(100_000_000)
result2 = cpu_bound_task(100_000_000)
print(f"Синхронно: {time.time() - start:.2f}s") # ~4.5s
# С потоками (два потока) — НЕ быстрее!
def thread_worker():
cpu_bound_task(100_000_000)
start = time.time()
threads = []
for _ in range(2):
t = threading.Thread(target=thread_worker)
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"С потоками: {time.time() - start:.2f}s") # ~5.5s (МЕДЛЕННЕЕ!)
# Почему медленнее? Overhead на переключение контекста
Визуально:
Без GIL (Java, Go):
Поток 1: [Код] [Код] [Код] [Код]
Поток 2: [Код] [Код] [Код] → Параллельно!
С GIL (Python):
Поток 1: [Код-----] [Ожидание]
Поток 2: [Код-----] [Ожидание]
→ Последовательно, плюс overhead переключения
2. Почему async не спасает от GIL
Async в Python — это не многопоточность, а кооперативная многозадачность на одном потоке:
import asyncio
import time
async def cpu_bound_async(n):
"""Async функция, но выполняется синхронно!"""
total = 0
for i in range(n):
total += i * i
return total
async def main():
# Оба вычисления выполняются ПОСЛЕДОВАТЕЛЬНО на одном потоке
start = time.time()
# Это не запустится параллельно!
result1 = await cpu_bound_async(100_000_000)
result2 = await cpu_bound_async(100_000_000)
print(f"Async: {time.time() - start:.2f}s") # ~4.5s (как синхронно)
asyncio.run(main())
Ключевой момент: await не освобождает GIL во время выполнения CPU-bound кода. Контекст переключается только на операциях I/O:
async def bad_async():
# ❌ await НЕ переключает контекст — это просто ждёт результата
result = await cpu_bound_async(1_000_000) # Блокирует!
async def good_async():
# ✅ await переключает контекст на операции I/O
data = await fetch_from_api() # Освобождает GIL во время ожидания
3. Сравнение подходов для CPU-bound
import time
import multiprocessing
from concurrent.futures import ProcessPoolExecutor
def cpu_work(n):
"""Тяжелые вычисления"""
return sum(i * i for i in range(n))
n = 100_000_000
# 1. Синхронно
print("\n1. СИНХРОННО:")
start = time.time()
result1 = cpu_work(n)
result2 = cpu_work(n)
print(f"Время: {time.time() - start:.2f}s") # ~4.5s
# 2. С потоками (НЕ работает для CPU-bound)
print("\n2. С ПОТОКАМИ (плохо для CPU-bound):")
from threading import Thread
start = time.time()
threads = []
for _ in range(2):
t = Thread(target=cpu_work, args=(n,))
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"Время: {time.time() - start:.2f}s") # ~5-6s (медленнее!)
# 3. С процессами (работает!)
print("\n3. С ПРОЦЕССАМИ (хорошо для CPU-bound):")
start = time.time()
with ProcessPoolExecutor(max_workers=2) as executor:
futures = [executor.submit(cpu_work, n) for _ in range(2)]
results = [f.result() for f in futures]
print(f"Время: {time.time() - start:.2f}s") # ~2.3s (в 2 раза быстрее!)
4. Async для I/O-bound задач (работает!)
import asyncio
import aiohttp
import time
async def fetch_url(session, url):
"""Асинхронный запрос (I/O bound)"""
async with session.get(url) as response:
return await response.text()
async def main():
urls = [f"https://httpbin.org/delay/2" for _ in range(5)]
# Синхронно: 5 * 2 = 10 секунд
# Асинхронно: ~2 секунды (параллельно!)
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"Async I/O: {time.time() - start:.2f}s") # ~2s
asyncio.run(main())
5. Таблица: когда что использовать
| Тип задачи | Проблема | Решение | Результат |
|---|---|---|---|
| I/O-bound (API, БД, файлы) | Блокирующие операции | Async/await | 5-10x ускорение |
| CPU-bound (вычисления) | GIL | Multiprocessing | 2-4x ускорение (в зависимости от ядер) |
| Mixed | И I/O и CPU | async + ProcessPool | Комбинированное решение |
6. Практический пример: правильное использование
import asyncio
from concurrent.futures import ProcessPoolExecutor
import aiohttp
# CPU-bound функция (выполнять в отдельном процессе)
def heavy_computation(data):
"""Медленные вычисления"""
result = 0
for item in data:
result += sum(i * i for i in range(item))
return result
# I/O-bound функция (выполнять асинхронно)
async def fetch_and_process():
"""Получить данные и обработать"""
# 1. Асинхронно получить данные
async with aiohttp.ClientSession() as session:
async with session.get(https://api.example.com/data) as resp:
data = await resp.json()
# 2. CPU-bound обработка в отдельном процессе
loop = asyncio.get_event_loop()
with ProcessPoolExecutor() as executor:
result = await loop.run_in_executor(
executor,
heavy_computation,
data
)
return result
asyncio.run(fetch_and_process())
7. Современное решение: Python 3.13+ (экспериментально)
Python 3.13 вводит --disable-gil флаг, который отключает GIL:
# Python 3.13+
python --disable-gil script.py
# Тогда async/threading/multiprocessing становятся истинно параллельными
Но это ещё в экспериментальной стадии.
8. Альтернативы
# 1. Использовать Cython или Numba для ускорения
from numba import jit
@jit(nopython=True)
def fast_cpu_work(n):
return sum(i * i for i in range(n))
# 2. Использовать numpy для векторизации
import numpy as np
arr = np.arange(100_000_000)
result = np.sum(arr ** 2) # Быстро, не зависит от GIL
# 3. Использовать многопроцессность
from multiprocessing import Pool
with Pool(4) as p:
results = p.map(cpu_work, [25_000_000] * 4) # 4 процесса
Итоговая таблица
Задача | Синхронный | Многопоточность | Async | Multiprocessing
| async/await | (Threading) | |
────────────────|────────────|─────────────────|────────|─────────────────
I/O операции | ❌ | ✅* | ✅✅✅✅| ✅
CPU вычисления | ❌ | ❌ | ❌ | ✅✅✅✅
Mixed | ❌ | ❌ | ✅ | ✅✅
* Threading работает для I/O, но медленнее async
Вывод
Async/await в Python работает только для I/O-bound задач из-за GIL. Для CPU-bound задач используй:
- Multiprocessing — несколько процессов, каждый со своим GIL
- Numba/Cython — компилированный код, минует GIL
- External services — передай работу C/C++/Rust библиотекам
Попытка использовать async для CPU задач не даст ускорения и добавит сложности кода.