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

Возможна ли ситуация, когда обработка I/O bound задач быстрее в потоках, чем кооперативно в asyncio

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

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

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

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

Возможна ли ситуация, когда обработка I/O bound задач быстрее в потоках, чем кооперативно в asyncio?

Да, есть конкретные ситуации, где многопоточность может быть быстрее. Это контринтуитивно, но случается.

1. Основное правило нарушается редко

Обычно asyncio быстрее: для чистых I/O операций asyncio имеет преимущество. Но существуют исключения.

2. Когда потоки могут быть быстрее

Ситуация 1: CPU-bound задачи в asyncio

import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor

# Чистые CPU-bound вычисления
def heavy_computation():
    return sum(i**2 for i in range(10_000_000))

# asyncio БЕЗ asyncio.sleep() → занимает весь процессор
async def async_cpu_bound():
    # Выполняется БЕЗ передачи управления
    # Asyncio не имеет преимущества
    return sum(i**2 for i in range(10_000_000))

async def async_version(n):
    tasks = [asyncio.create_task(async_cpu_bound()) for _ in range(n)]
    return await asyncio.gather(*tasks)

def thread_version(n):
    with ThreadPoolExecutor(max_workers=4) as executor:
        return list(executor.map(lambda _: heavy_computation(), range(n)))

# asyncio: ~10s (одна нить, последовательно)
start = time.time()
asyncio.run(async_version(2))
print(f"asyncio CPU-bound: {time.time() - start:.2f}s")

# Threading: может быть быстрее благодаря context switch
start = time.time()
thread_version(2)
print(f"Threading CPU-bound: {time.time() - start:.2f}s")

Ситуация 2: Блокирующие I/O библиотеки

import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
import sqlite3
import time

# Блокирующий SQL запрос
def blocking_db_query():
    conn = sqlite3.connect(':memory:')
    conn.execute('CREATE TABLE test (id INTEGER, name TEXT)')
    for i in range(1000):
        conn.execute(f"INSERT INTO test VALUES ({i}, 'name{i}')")
    results = conn.execute('SELECT * FROM test WHERE id > 500').fetchall()
    conn.close()
    return results

# asyncio с блокирующей функцией → ПЛОХО
async def async_with_blocking(n):
    tasks = [asyncio.create_task(asyncio.to_thread(blocking_db_query)) for _ in range(n)]
    return await asyncio.gather(*tasks)

# Threading: просто используем threading
def threading_approach(n):
    with ThreadPoolExecutor(max_workers=4) as executor:
        return list(executor.map(lambda _: blocking_db_query(), range(n)))

# asyncio: ~3-4s с asyncio.to_thread
start = time.time()
asyncio.run(async_with_blocking(4))
print(f"asyncio (to_thread): {time.time() - start:.2f}s")

# Threading: ~3-4s (параллельно)
start = time.time()
threading_approach(4)
print(f"Threading: {time.time() - start:.2f}s")

# При большом количестве потоков asyncio начинает проигрывать

3. Проблема: GIL в asyncio при CPU-bound

Global Interpreter Lock в Python означает, что даже с asyncio CPU-bound задачи не выполняются параллельно.

4. Ситуация 3: Много context switch'ей в asyncio

import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor

# Много коротких I/O операций с большим overhead
async def tiny_io_async():
    await asyncio.sleep(0.00001)  # 10 микросекунд

def tiny_io_sync():
    time.sleep(0.00001)  # 10 микросекунд

async def asyncio_version(n):
    tasks = [asyncio.create_task(tiny_io_async()) for _ in range(n)]
    await asyncio.gather(*tasks)

def threading_version(n):
    with ThreadPoolExecutor(max_workers=10) as executor:
        list(executor.map(lambda _: tiny_io_sync(), range(n)))

# asyncio: Overhead может быть больше, чем сама работа
start = time.time()
asyncio.run(asyncio_version(10000))
print(f"asyncio (tiny tasks): {time.time() - start:.2f}s")

# Threading: примерно одинаково или даже быстрее
start = time.time()
threading_version(10000)
print(f"Threading (tiny tasks): {time.time() - start:.2f}s")

5. Ситуация 4: Несовместимые библиотеки

import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
import requests
import time

# requests НЕ имеет асинхронной версии
def sync_fetch(url):
    return requests.get(url).status_code

async def async_bad_approach(urls):
    # Прямой вызов блокирующей функции
    # Это ЗАБЛОКИРУЕТ весь event loop
    results = []
    for url in urls:
        results.append(sync_fetch(url))  # Блокирует!
    return results

async def async_good_approach(urls):
    # Правильный способ: использовать asyncio.to_thread
    tasks = [asyncio.to_thread(sync_fetch, url) for url in urls]
    return await asyncio.gather(*tasks)

def threading_approach(urls):
    with ThreadPoolExecutor(max_workers=10) as executor:
        return list(executor.map(sync_fetch, urls))

urls = [f"https://httpbin.org/delay/1?id={i}" for i in range(10)]

# asyncio (BAD): ~10s (последовательно, блокирует)
start = time.time()
asyncio.run(async_bad_approach(urls))
print(f"asyncio (BAD): {time.time() - start:.2f}s")

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

# Threading: ~1s (параллельно)
start = time.time()
threading_approach(urls)
print(f"Threading: {time.time() - start:.2f}s")

6. Практический сценарий: Смешанная нагрузка

import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor

# CPU-bound
def cpu_work():
    return sum(i**2 for i in range(10_000_000))

# I/O-bound (имитация)
def io_work():
    time.sleep(0.5)
    return "done"

async def asyncio_mixed():
    # 2 CPU-bound + 2 I/O-bound
    tasks = [
        asyncio.to_thread(cpu_work),
        asyncio.to_thread(cpu_work),
        asyncio.sleep(0.5),
        asyncio.sleep(0.5),
    ]
    return await asyncio.gather(*tasks)

def threading_mixed():
    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = [
            executor.submit(cpu_work),
            executor.submit(cpu_work),
            executor.submit(io_work),
            executor.submit(io_work),
        ]
        return [f.result() for f in futures]

# asyncio: ~5s (CPU работают в потоках, параллельно)
start = time.time()
asyncio.run(asyncio_mixed())
print(f"asyncio (mixed): {time.time() - start:.2f}s")

# Threading: ~2.5-3s (все работают параллельно)
start = time.time()
threading_mixed()
print(f"Threading (mixed): {time.time() - start:.2f}s")

7. Итоговая таблица: когда что быстрее

СценарийasyncioThreadingMultiprocessing
Pure I/O (HTTP)Очень быстроНормальноМедленно
CPU-boundМедленноМожет быть быстрееБыстро
I/O + CPU mixЗависитЧасто быстрееБыстро
Blocking I/O libНормальноБыстроМедленно
Много микротасокНормальноМожет быть медленнееОчень медленно
1000+ одновременных I/OБыстроМедленноМедленно

8. Когда потоки действительно быстрее asyncio

1. Смешанная нагрузка с CPU-bound

Если у вас есть 50% CPU-bound + 50% I/O-bound работа, потоки могут быть быстрее, потому что:

  • asyncio выполняет CPU последовательно
  • Потоки выполняют всё параллельно (благодаря OS-level scheduling)

2. Много мелких I/O операций

Когда overhead создания asyncio tasks больше, чем сама работа, потоки могут быть проще и быстрее.

3. Большое количество одновременных задач

Когда одновременных задач 10000+, asyncio начинает терять производительность из-за управления event loop.

4. Несовместимые библиотеки

Если вы вынуждены оборачивать блокирующий код в asyncio.to_thread(), вы фактически используете потоки, а не чистый asyncio.

9. Рекомендация: как выбрать

# Используй asyncio для:
# - Чистых I/O задач (HTTP API, асинхронные БД)
# - Масштабируемости (1000+ одновременных соединений)
# - Когда все библиотеки асинхронные

# Используй Threading для:
# - Смешанной нагрузки (I/O + CPU)
# - Блокирующих библиотек (requests, sqlite3)
# - Существующего многопоточного кода
# - Когда нужна простота

# Используй Multiprocessing для:
# - Pure CPU-bound (расчёты, обработка)
# - Реального параллелизма (обойти GIL)

# Гибридный подход:
async def hybrid():
    loop = asyncio.get_event_loop()
    
    # Асинхронная I/O
    # async with aiohttp.ClientSession() as session:
    #     response = await session.get(url)
    
    # CPU-bound в отдельном потоке
    result = await asyncio.to_thread(cpu_task)
    # Благодаря asyncio, CPU не блокирует I/O

Итог

Да, потоки могут быть быстрее asyncio:

  1. Смешанная нагрузка — потоки работают параллельно, asyncio последовательна для CPU
  2. CPU-bound задачи — asyncio одна нить, потоки используют OS scheduling
  3. Блокирующие I/O операции — если используются неправильно
  4. Много микротасок — overhead может быть больше работы
  5. Несовместимые библиотеки — requests вместо aiohttp

Но для pure I/O: asyncio всегда быстрее. Просто используйте его правильно с асинхронными библиотеками.

Возможна ли ситуация, когда обработка I/O bound задач быстрее в потоках, чем кооперативно в asyncio | PrepBro