Основы asyncio: корутины и await
Зачем нужна асинхронность
В синхронном коде каждая операция блокирует выполнение программы до своего завершения. Если программа ждёт ответ от сервера 2 секунды — она простаивает всё это время, не делая ничего полезного.
Асинхронное программирование позволяет во время ожидания одной операции выполнять другие задачи. Это особенно важно для I/O-bound задач: сетевые запросы, чтение файлов, работа с базами данных.
# Синхронный подход — последовательное ожидание
import time
def fetch_data(url: str) -> str:
print(f"Запрос к {url}...")
time.sleep(2) # Simulating network delay
return f"Данные от {url}"
# Each call waits for the previous one
result1 = fetch_data("api/users") # 2 sec
result2 = fetch_data("api/orders") # 2 sec
result3 = fetch_data("api/products") # 2 sec
# Total: ~6 seconds
# Асинхронный подход — конкурентное ожидание
import asyncio
async def fetch_data(url: str) -> str:
print(f"Запрос к {url}...")
await asyncio.sleep(2) # Non-blocking sleep
return f"Данные от {url}"
async def main() -> None:
# All three run concurrently
results = await asyncio.gather(
fetch_data("api/users"),
fetch_data("api/orders"),
fetch_data("api/products"),
)
print(results)
# Total: ~2 seconds
asyncio.run(main())
Корутины: async def
Корутина (coroutine) — это функция, объявленная с ключевым словом async def. Вызов корутины не выполняет её код, а возвращает объект корутины, который нужно запустить через event loop.
import asyncio
async def greet(name: str) -> str:
"""A simple coroutine."""
return f"Привет, {name}!"
# Calling a coroutine returns a coroutine object, NOT the result
coro = greet("Мир")
print(type(coro)) # <class 'coroutine'>
# To actually execute it, use asyncio.run()
result = asyncio.run(greet("Мир"))
print(result) # Привет, Мир!
Важное отличие: обычная функция выполняется сразу при вызове. Корутина — только когда её await-ят или передают в event loop.
async def compute(x: int, y: int) -> int:
"""Coroutine that simulates computation."""
print(f"Вычисляю {x} + {y}...")
await asyncio.sleep(1) # Simulate async work
return x + y
async def main() -> None:
# await executes the coroutine and returns the result
result = await compute(3, 7)
print(f"Результат: {result}")
asyncio.run(main())
Ключевое слово await
await — оператор, который приостанавливает выполнение текущей корутины до завершения awaitable-объекта. Использовать await можно только внутри async def.
Awaitable-объекты в Python:
- Корутины (
async def) - Задачи (
asyncio.Task) - Фьючерсы (
asyncio.Future)
import asyncio
async def step_one() -> str:
print("Шаг 1: начало")
await asyncio.sleep(1)
print("Шаг 1: завершён")
return "данные шага 1"
async def step_two(data: str) -> str:
print(f"Шаг 2: получил '{data}'")
await asyncio.sleep(1)
print("Шаг 2: завершён")
return f"обработано: {data}"
async def pipeline() -> None:
# Sequential execution — step_two waits for step_one
data = await step_one()
result = await step_two(data)
print(f"Итог: {result}")
asyncio.run(pipeline())
# Шаг 1: начало
# Шаг 1: завершён
# Шаг 2: получил 'данные шага 1'
# Шаг 2: завершён
# Итог: обработано: данные шага 1
Event Loop — цикл событий
Event loop — это ядро asyncio. Он управляет выполнением корутин, переключая между ними в точках await. Когда одна корутина ждёт I/O, event loop запускает другую.
import asyncio
async def worker(name: str, delay: float) -> str:
print(f"[{name}] Старт")
await asyncio.sleep(delay) # Yield control to event loop
print(f"[{name}] Готово после {delay}с")
return f"{name}: выполнено"
async def main() -> None:
# Run concurrently — event loop switches between coroutines
results = await asyncio.gather(
worker("A", 3),
worker("B", 1),
worker("C", 2),
)
for r in results:
print(r)
asyncio.run(main())
# [A] Старт
# [B] Старт
# [C] Старт
# [B] Готово после 1с
# [C] Готово после 2с
# [A] Готово после 3с
Обратите внимание: все три воркера стартовали одновременно, а завершились в порядке длительности задержки, а не в порядке запуска.
asyncio.run() — точка входа
asyncio.run() — рекомендуемый способ запуска асинхронного кода из синхронного контекста (Python 3.7+). Он создаёт новый event loop, запускает переданную корутину и закрывает loop после завершения.
import asyncio
async def fetch_user(user_id: int) -> dict:
"""Simulate fetching user from database."""
await asyncio.sleep(0.5)
return {"id": user_id, "name": f"User_{user_id}"}
async def main() -> None:
users = []
for uid in range(1, 4):
user = await fetch_user(uid)
users.append(user)
print(users)
# Entry point — call from synchronous code
if __name__ == "__main__":
asyncio.run(main())
Важные правила:
asyncio.run()нельзя вызвать, если event loop уже запущен (например, в Jupyter — используйтеawait main())- В программе обычно один вызов
asyncio.run()в точке входа - Все остальные корутины запускаются через
awaitилиcreate_task()
Синхронный vs асинхронный: сравнение
| Характеристика | Синхронный код | Асинхронный код |
|---|---|---|
| Ожидание I/O | Блокирует поток | Переключается на другую задачу |
| Параллелизм | Требует потоков/процессов | Конкурентность в одном потоке |
| Сложность | Простой и линейный | Требует async/await |
| Лучше для | CPU-bound, простые скрипты | I/O-bound, сетевые приложения |
| Отладка | Стандартные инструменты | Специальные (asyncio debug mode) |
import asyncio
import time
# Synchronous version
def sync_download(urls: list[str]) -> list[str]:
results = []
for url in urls:
time.sleep(1) # Simulate download
results.append(f"Downloaded {url}")
return results
# Asynchronous version
async def async_download(urls: list[str]) -> list[str]:
async def download_one(url: str) -> str:
await asyncio.sleep(1) # Simulate download
return f"Downloaded {url}"
return await asyncio.gather(*[download_one(u) for u in urls])
urls = [f"https://example.com/page{i}" for i in range(5)]
# Sync: ~5 seconds
start = time.perf_counter()
sync_download(urls)
print(f"Sync: {time.perf_counter() - start:.2f}с")
# Async: ~1 second
start = time.perf_counter()
asyncio.run(async_download(urls))
print(f"Async: {time.perf_counter() - start:.2f}с")
Частые ошибки начинающих
Забыли await
async def get_data() -> str:
await asyncio.sleep(1)
return "данные"
async def main() -> None:
# BUG: missing await — result is a coroutine object!
result = get_data()
print(result) # <coroutine object get_data at 0x...>
# Correct:
result = await get_data()
print(result) # данные
Блокирующий вызов внутри корутины
import time
async def bad_example() -> None:
# BAD: time.sleep blocks the entire event loop!
time.sleep(5)
async def good_example() -> None:
# GOOD: asyncio.sleep yields control to event loop
await asyncio.sleep(5)
Если нужно вызвать блокирующую функцию из async-кода, используйте run_in_executor:
import asyncio
import time
def blocking_io() -> str:
time.sleep(2)
return "результат"
async def main() -> None:
loop = asyncio.get_running_loop()
# Run blocking function in a thread pool
result = await loop.run_in_executor(None, blocking_io)
print(result)