Что самое страшное может случиться в Event Loop?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Опасности Event Loop в Python asyncio
Что такое Event Loop
Event Loop — это сердце асинхронного программирования в Python. Это бесконечный цикл, который:
- Проверяет готовые корутины
- Выполняет их до первой операции ввода-вывода (await)
- Переходит к следующей корутине
- Повторяет процесс
Одна из главных особенностей: 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.