Возможна ли ситуация, когда обработка I/O bound задач быстрее в потоках, чем кооперативно в asyncio
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Возможна ли ситуация, когда обработка 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. Итоговая таблица: когда что быстрее
| Сценарий | asyncio | Threading | Multiprocessing |
|---|---|---|---|
| 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:
- Смешанная нагрузка — потоки работают параллельно, asyncio последовательна для CPU
- CPU-bound задачи — asyncio одна нить, потоки используют OS scheduling
- Блокирующие I/O операции — если используются неправильно
- Много микротасок — overhead может быть больше работы
- Несовместимые библиотеки — requests вместо aiohttp
Но для pure I/O: asyncio всегда быстрее. Просто используйте его правильно с асинхронными библиотеками.