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

Что самое страшное может случиться в Event Loop?

1.8 Middle🔥 121 комментариев
#Python Core

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

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

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

Опасности Event Loop в Python asyncio

Что такое Event Loop

Event Loop — это сердце асинхронного программирования в Python. Это бесконечный цикл, который:

  1. Проверяет готовые корутины
  2. Выполняет их до первой операции ввода-вывода (await)
  3. Переходит к следующей корутине
  4. Повторяет процесс

Одна из главных особенностей: Event Loop однопоточный. Это создаёт множество подводных камней.

Главная опасность: блокирующие операции

Проблема 1: Блокирование всего Event Loop

import asyncio
import time
from datetime import datetime

async def slow_blocking_operation():
    """Блокирующая операция внутри async функции - ОПАСНО!"""
    print(f"Начало: {datetime.now()}")
    time.sleep(5)  # БЛОКИРУЕТ весь Event Loop на 5 секунд!
    print(f"Конец: {datetime.now()}")

async def fast_operation():
    """Эта операция будет ждать 5 секунд, прежде чем начать"""
    print(f"Fast началась: {datetime.now()}")
    await asyncio.sleep(0.1)
    print(f"Fast закончилась: {datetime.now()}")

async def main():
    # Оба вызова запустятся параллельно, но на практике...
    await asyncio.gather(
        slow_blocking_operation(),  # Заблокирует Event Loop на 5 сек
        fast_operation()            # Не начнется до конца блокировки
    )

# Результат:
# Начало: 14:00:00
# Конец: 14:00:05           <- 5 секунд ждёт!
# Fast началась: 14:00:05   <- Fast начинается только после блокировки
# Fast закончилась: 14:00:05

Проблема: time.sleep(5) блокирует весь Event Loop. Остальные корутины не могут работать.

Решение: использовать asyncio.sleep() вместо time.sleep():

async def proper_async_operation():
    """Правильный способ - асинхронное ожидание"""
    print(f"Начало: {datetime.now()}")
    await asyncio.sleep(5)  # Не блокирует Event Loop!
    print(f"Конец: {datetime.now()}")

async def main():
    await asyncio.gather(
        proper_async_operation(),  # 5 секунд
        fast_operation()           # 0.1 секунд
    )

# Результат: обе операции выполняются параллельно
# Fast закончится в 0.1 сек, а первая - в 5 сек

Проблема 2: Синхронные операции БД и сетевые запросы

import asyncio
import requests
from sqlalchemy import text
from sqlalchemy.orm import Session

# ❌ ОПАСНО: синхронный запрос в async функции
async def get_user_data_blocking(db: Session, user_id: int):
    # Это заблокирует Event Loop на время запроса к БД
    user = db.query(User).filter(User.id == user_id).first()
    
    # Это тоже заблокирует
    response = requests.get(f"https://api.example.com/user/{user_id}")
    
    return {"db_user": user, "api_response": response.json()}

# ✅ ПРАВИЛЬНО: использовать асинхронные драйверы
import aiohttp
from sqlalchemy.ext.asyncio import AsyncSession

async def get_user_data_async(
    db: AsyncSession,
    user_id: int,
    session: aiohttp.ClientSession
):
    # Асинхронный запрос к БД (не блокирует Event Loop)
    result = await db.execute(
        select(User).where(User.id == user_id)
    )
    user = result.scalar_one_or_none()
    
    # Асинхронный HTTP запрос (не блокирует Event Loop)
    async with session.get(f"https://api.example.com/user/{user_id}") as resp:
        api_response = await resp.json()
    
    return {"db_user": user, "api_response": api_response}

Опасность 2: Deadlock (взаимная блокировка)

Сценарий deadlock с asyncio.Lock

import asyncio

lock = asyncio.Lock()

async def deadlock_scenario():
    """Попытка захватить lock дважды - DEADLOCK!"""
    async with lock:
        print("Захватил lock")
        
        # Попытка захватить тот же lock ещё раз - ждём вечно
        async with lock:
            print("Это никогда не выполнится")

# asyncio.run(deadlock_scenario())  # Зависнет навсегда

Решение: использовать RLock (Reentrant Lock) или избегать повторного захвата:

import asyncio

rlock = asyncio.RLock()  # Один поток может захватить несколько раз

async def no_deadlock():
    """RLock позволяет переентерировать"""
    async with rlock:
        print("Захватил rlock")
        async with rlock:
            print("Опять захватил - OK!")

await no_deadlock()

Опасность 3: Race Conditions

import asyncio

counter = 0

async def increment_counter():
    """Race condition: несколько корутин меняют counter одновременно"""
    global counter
    # Прочитать
    temp = counter
    # Пауза (Event Loop может переключиться на другую корутину!)
    await asyncio.sleep(0)
    # Записать
    counter = temp + 1

async def main():
    global counter
    counter = 0
    
    # Запустить 10 корутин одновременно
    await asyncio.gather(*[increment_counter() for _ in range(10)])
    
    print(f"Counter: {counter}")  # Ожидаем 10, получим < 10
    # Результат может быть: 3, 5, 8 - зависит от расписания

await main()

Решение: использовать Lock для синхронизации:

import asyncio

lock = asyncio.Lock()
counter = 0

async def increment_counter_safe():
    """Безопасное инкрементирование"""
    global counter
    async with lock:  # Только одна корутина может выполняться
        temp = counter
        await asyncio.sleep(0)
        counter = temp + 1

async def main():
    global counter
    counter = 0
    
    await asyncio.gather(*[increment_counter_safe() for _ in range(10)])
    print(f"Counter: {counter}")  # Теперь точно 10

Опасность 4: Callback адь (Callback Hell)

# ❌ Старый стиль с колбэками - сложно отлавливать ошибки
def old_style():
    future = asyncio.sleep(1)
    def on_complete(f):
        print("Шаг 1")
        future2 = asyncio.sleep(1)
        def on_complete2(f2):
            print("Шаг 2")
        future2.add_done_callback(on_complete2)
    future.add_done_callback(on_complete)

# ✅ Современный стиль с async/await - чистый и понятный
async def modern_style():
    await asyncio.sleep(1)
    print("Шаг 1")
    await asyncio.sleep(1)
    print("Шаг 2")

Опасность 5: Uncaught exceptions в Task

import asyncio

async def failing_task():
    await asyncio.sleep(1)
    raise ValueError("Что-то пошло не так")

async def main():
    # Если не обработать исключение, оно молча исчезнет
    task = asyncio.create_task(failing_task())
    # Эта программа закончится, и исключение никогда не будет обработано

# asyncio.run(main())  # Никаких ошибок видно не будет!

Решение: всегда ждите результат или используйте try-except:

async def main_fixed():
    try:
        task = asyncio.create_task(failing_task())
        await task  # Исключение будет выброшено здесь
    except ValueError as e:
        print(f"Ошибка: {e}")

await main_fixed()

Чек-лист безопасности Event Loop

✓ Никогда не используйте time.sleep() — только await asyncio.sleep() ✓ Используйте асинхронные драйверы для БД и HTTP (asyncpg, aiohttp) ✓ Используйте asyncio.Lock для общего состояния ✓ Всегда обрабатывайте исключения в Task ✓ Профилируйте Event Loop на блокировки: используйте asyncio.get_running_loop().slow_callback_duration ✓ Не запускайте тяжёлые вычисления - выносите в loop.run_in_executor()

Итог

SelfEventLoop — это мощный инструмент, но опасный. Главное правило: никогда не блокируйте Event Loop. Все операции ввода-вывода должны быть асинхронными, иначе приложение становится однопоточным и теряет все преимущества async/await.