Короткий ответ
Блокирующий вызов внутри корутины занимает единственный поток event loop целиком до своего завершения, не давая циклу переключиться на другие готовые задачи — это «замораживает» обработку всех остальных конкурентных запросов на это время.
Как это работает подробнее
Event loop asyncio однопоточный — он переключается между корутинами только в точках await на неблокирующих операциях; обычный синхронный вызов, физически ожидающий ответа, не содержит await и не отдаёт управление, поэтому процесс «зависает» для всех остальных задач.
Пример кода
example-268.py
import asyncio, time
async def bad_handler():
time.sleep(2) # блокирующий вызов - ОШИБКА в асинхронном коде, для анализа
return "done"
async def good_handler():
await asyncio.sleep(2) # неблокирующее ожидание, event loop свободен для других задач
async def main():
start = time.perf_counter()
await asyncio.gather(good_handler(), good_handler())
print("good:", time.perf_counter() - start) # ~2с, задачи шли конкурентно
start = time.perf_counter()
await asyncio.gather(bad_handler(), bad_handler())
print("bad:", time.perf_counter() - start) # ~4с, выполнялись фактически последовательно
asyncio.run(main())Что использовать на практике
- Практика. Симптом в production — рост латентности и таймаутов у несвязанных запросов именно в моменты, когда где-то в коде выполняется медленный синхронный вызов; диагностируют через debug-режим asyncio, трассировку запросов и метрики задержки по эндпоинтам, не делающим сам медленный вызов.
Подводные камни
- Риски. Используют синхронные клиентские библиотеки внутри async-хендлеров «для простоты», не замечая проблему на малой нагрузке; как временное решение выполняют такие вызовы через
loop.run_in_executor, что устраняет блокировку event loop, но добавляет накладные расходы на поток.