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

В каком случае Event Loop в asyncio может быть заблокирован

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

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

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

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

# Event Loop блокировка в asyncio

Event Loop в asyncio может быть заблокирован, если в асинхронный код добавить синхронные операции. Это одна из наиболее частых ошибок при работе с asyncio. Разберём все случаи и как их избежать.

Как работает Event Loop

Event Loop — это цикл, который проверяет готовность операций и переключается между ними:

# Упрощённая модель Event Loop
while not stopped:
    # 1. Проверить, какие операции готовы (I/O, таймеры)
    ready = check_ready_operations()
    
    # 2. Выполнить одну операцию
    for task in ready:
        task.run()  # Это должно быть быстро!
    
    # 3. Если нет готовых операций, подождать
    sleep_until_next_ready()

Блокировка происходит, когда task.run() занимает слишком много времени.

1. Синхронные I/O операции

Самая частая ошибка — использовать синхронные функции в асинхронном коде:

Проблема: requests вместо httpx

import asyncio
import requests  # СИНХРОННЫЙ
import httpx     # АСИНХРОННЫЙ

async def fetch_data_wrong():
    # БЛОКИРУЕТ Event Loop на 1 сек
    response = requests.get("https://api.example.com/data")
    return response.json()

async def fetch_data_correct():
    # Не блокирует Event Loop
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.example.com/data")
    return response.json()

async def main():
    # Если использовать неправильный код:
    # Event Loop ждёт 1 сек, другие задачи не выполняются
    task1 = asyncio.create_task(fetch_data_wrong())
    task2 = asyncio.create_task(other_async_task())
    
    # task2 не стартует, пока task1 не вернёт управление!
    await asyncio.gather(task1, task2)

Проблема: синхронные database операции

from sqlalchemy import create_engine  # СИНХРОННЫЙ
from sqlalchemy.ext.asyncio import create_async_engine  # АСИНХРОННЫЙ

# Неправильно
engine = create_engine("postgresql://user:pass@localhost/db")

async def get_users_wrong():
    session = Session(engine)
    users = session.query(User).all()  # БЛОКИРУЕТ на 100ms
    return users

# Правильно
async_engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")

async def get_users_correct():
    async with AsyncSession(async_engine) as session:
        result = await session.execute(select(User))
        return result.scalars().all()

2. CPU-интенсивные операции

Даже без I/O, долгие вычисления блокируют Event Loop:

import asyncio
import time

# Проблема: длительные вычисления
async def fibonacci_wrong(n):
    """Блокирует Event Loop на 10+ сек"""
    if n < 2:
        return n
    return fibonacci_wrong(n-1) + fibonacci_wrong(n-2)

async def main():
    task1 = asyncio.create_task(fibonacci_wrong(40))  # ~40 сек на CPU
    task2 = asyncio.create_task(asyncio.sleep(1))  # Должно заснуть на 1 сек
    
    await asyncio.gather(task1, task2)
    # task2 не начнётся, пока task1 не закончится

Решение — переместить в отдельный процесс:

from concurrent.futures import ProcessPoolExecutor

def fibonacci(n):  # Синхронная функция
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

async def fibonacci_async(n):
    """Не блокирует Event Loop"""
    loop = asyncio.get_event_loop()
    with ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, fibonacci, n)
    return result

async def main():
    task1 = asyncio.create_task(fibonacci_async(40))  # На отдельном процессе
    task2 = asyncio.create_task(asyncio.sleep(1))  # Начнётся сразу
    
    await asyncio.gather(task1, task2)
    # task2 завершится в 1 сек, task1 будет выполняться параллельно

3. Синхронные файловые операции

import asyncio
import aiofiles
import time

# Неправильно: открытие больших файлов
async def read_file_wrong(path):
    with open(path, 'r') as f:  # БЛОКИРУЕТ на 100ms
        return f.read()

# Правильно
async def read_file_correct(path):
    async with aiofiles.open(path, 'r') as f:
        return await f.read()

async def demonstrate_blocking():
    # Если read_file_wrong:
    task1 = asyncio.create_task(read_file_wrong('large_file.txt'))  # 100ms
    
    start = time.time()
    task2 = asyncio.create_task(asyncio.sleep(0.05))  # 50ms
    
    await asyncio.gather(task1, task2)
    elapsed = time.time() - start
    
    print(f"Elapsed: {elapsed:.2f}s")  # 0.10s (task2 ждал task1)

4. Блокирующие библиотеки

import asyncio
import logging
import json

# Проблема: блокирующие библиотеки
async def process_data_wrong():
    data = {"users": list(range(1000000))}
    
    # json.dumps может блокировать на большых объёмах
    json_str = json.dumps(data)  # МОЖЕТ БЛОКИРОВАТЬ
    
    logging.info(json_str)  # БЛОКИРУЕТ файловый I/O
    
    # PIL обработка изображений
    from PIL import Image
    img = Image.open('large_image.jpg')  # БЛОКИРУЕТ
    img = img.resize((1920, 1080))  # БЛОКИРУЕТ
    
    return json_str

# Решение: run_in_executor
async def process_data_correct():
    loop = asyncio.get_event_loop()
    data = {"users": list(range(1000000))}
    
    # Запустить в thread pool
    json_str = await loop.run_in_executor(
        None,  # используем default ThreadPoolExecutor
        json.dumps,
        data
    )
    
    # Логирование (если критично для производительности)
    # await async_logger.info(json_str)
    
    return json_str

5. Блокирующие сетевые операции (socket)

import asyncio
import socket

# Проблема: использование socket напрямую
async def connect_wrong(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))  # БЛОКИРУЕТ
    sock.send(b"Hello")  # БЛОКИРУЕТ
    data = sock.recv(1024)  # БЛОКИРУЕТ
    return data

# Правильно: использовать asyncio.open_connection
async def connect_correct(host, port):
    reader, writer = await asyncio.open_connection(host, port)
    writer.write(b"Hello")
    await writer.drain()  # Ждём, пока буфер опустеет
    data = await reader.read(1024)
    writer.close()
    return data

6. Непрерывные вычисления (Busy Loop)

import asyncio

# Проблема: цикл без await
async def busy_loop_wrong():
    # Это БЛОКИРУЕТ Event Loop!
    for i in range(1_000_000_000):
        pass  # Нет await, Event Loop не может переключиться
    return "done"

# Решение: добавить периодические await
async def busy_loop_correct():
    for i in range(1_000_000_000):
        if i % 10000 == 0:  # Каждые 10k итераций
            await asyncio.sleep(0)  # Отдаёт контроль Event Loop'у
    return "done"

async def demonstrate():
    # Неправильный вариант
    task1 = asyncio.create_task(busy_loop_wrong())
    task2 = asyncio.create_task(asyncio.sleep(0.1))
    
    done, pending = await asyncio.wait(
        [task1, task2],
        timeout=1
    )
    # task2 не завершится в отведённое время!
    print(f"Done: {len(done)}, Pending: {len(pending)}")

7. Блокирующие операции в обработчиках

from fastapi import FastAPI, BackgroundTasks
import time

app = FastAPI()

# Проблема: медленная операция в эндпоинте
@app.get("/wrong")
async def endpoint_wrong():
    time.sleep(1)  # БЛОКИРУЕТ Event Loop на 1 сек
    return {"status": "ok"}

# Решение: асинхронная функция
@app.get("/correct")
async def endpoint_correct():
    await asyncio.sleep(1)  # Отдаёт контроль другим задачам
    return {"status": "ok"}

# Или использовать background task
@app.get("/background")
async def endpoint_with_bg(background_tasks: BackgroundTasks):
    background_tasks.add_task(slow_operation)  # Выполнится в фоне
    return {"status": "processing"}

def slow_operation():
    time.sleep(1)

Как обнаружить блокировку Event Loop

import asyncio
import logging

# Логирование медленных операций
logging.basicConfig(level=logging.DEBUG)

loop = asyncio.get_event_loop()

# Метод 1: Slow callback detection
loop.slow_callback_duration = 0.1  # Логирует callback'и > 100ms

# Метод 2: Вручную мониторить время
async def check_blocking():
    import time
    
    start = time.perf_counter()
    await asyncio.sleep(0)  # Отдаём контроль
    elapsed = time.perf_counter() - start
    
    if elapsed > 0.05:  # > 50ms значит Event Loop был занят
        print(f"Event Loop заблокирован на {elapsed*1000:.1f}ms")

# Метод 3: Использовать asyncio.Task
task = asyncio.create_task(check_blocking())
time.sleep(1)  # Это БЛОКИРУЕТ
await task  # task будет показывать блокировку

Таблица асинхронных альтернатив

┌──────────────────────┬─────────────────────┬──────────────────┐
│ Синхронное           │ Асинхронное         │ Альтернатива     │
├──────────────────────┼─────────────────────┼──────────────────┤
│ requests.get()       │ httpx.get()         │ await httpx...   │
│ open()               │ aiofiles.open()     │ await aiofiles...│
│ socket.connect()     │ asyncio.open_con()  │ await asyncio... │
│ time.sleep(n)        │ asyncio.sleep(n)    │ await asyncio... │
│ db.query()           │ async db.query()    │ await async...   │
│ json.dumps()         │ loop.run_executor() │ await loop...    │
│ fibonacci(n)         │ loop.run_executor() │ await loop...    │
│ logging.info()       │ async_logger        │ await async...   │
└──────────────────────┴─────────────────────┴──────────────────┘

Ключевые выводы

  1. Никогда не используйте time.sleep() в async коде — используйте await asyncio.sleep()
  2. Используйте асинхронные библиотеки — httpx, aiofiles, asyncpg
  3. Для CPU-интенсивных операций — используйте run_in_executor()
  4. Для файловых операций — используйте aiofiles
  5. Добавляйте await asyncio.sleep(0) в длительные циклы
  6. Мониторьте блокировки — используйте slow_callback_duration

Помните: Event Loop — это одна threads, которая должна выполняться очень быстро!

В каком случае Event Loop в asyncio может быть заблокирован | PrepBro