Easy📖Теория4 min

Основы asyncio: корутины и await

Введение в асинхронное программирование: async def, await, asyncio.run(), event loop и сравнение с синхронным кодом

Основы 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)

Проверь себя

🧪

Что делает asyncio.run(main())?

🧪

Что произойдёт при вызове `result = greet('Мир')`, если greet — корутина (async def)?

🧪

Почему нельзя использовать time.sleep() внутри корутины?

🧪

Где можно использовать ключевое слово `await`?

🧪

Для какого типа задач асинхронное программирование даёт наибольший выигрыш?