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

Почему асинхронность не подходит для CPU bound задач?

1.6 Junior🔥 191 комментариев
#Асинхронность и многопоточность

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

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

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

Почему асинхронность не подходит для 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/await5-10x ускорение
CPU-bound (вычисления)GILMultiprocessing2-4x ускорение (в зависимости от ядер)
MixedИ I/O и CPUasync + 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 задач используй:

  1. Multiprocessing — несколько процессов, каждый со своим GIL
  2. Numba/Cython — компилированный код, минует GIL
  3. External services — передай работу C/C++/Rust библиотекам

Попытка использовать async для CPU задач не даст ускорения и добавит сложности кода.