Python middle

Глава 1. Введение в асинхронное программирование

Артём сидел перед монитором и смотрел на график нагрузки. Сервис, который он поддерживал, каждое утро превращался в узкое горлышко: сотни одновременных запросов к внешнему API, каждый ждал ответа полторы–две секунды, а Python упрямо держал поток занятым. Коллега из соседней команды, глянув через плечо, бросил фразу, от которой у Артёма щёлкнуло в голове: «Ты же не ждёшь, пока чайник закипит, стоя над ним. Идёшь делать другое. Так и здесь».

Именно так асинхронное программирование попадает в жизнь разработчика — не как модное слово из вакансии, а как инструмент, когда программа большую часть времени ничего не считает, а ждёт: сеть, диск, ответ базы, таймер. Синхронный код в такой ситуации похож на официанта, который принимает заказ у первого столика и стоит у него, пока кухня готовит, хотя в зале ещё двадцать гостей. Асинхронный — принимает заказы, передаёт их на кухню и, пока одно блюдо готовится, обслуживает остальных.

Для мидла asyncio — не «ещё одна библиотека», а способ мыслить о параллелизме в одном потоке. Потоки и процессы остаются нужными, когда есть тяжёлая CPU-работа. Но типичный веб-сервис, бот, интеграция с десятком REST-эндпоинтов — это ожидание. Здесь asyncio даёт плотность: один процесс обслуживает тысячи соединений, не раздувая память на стек каждого потока и не играя в рулетку с блокировками.

Чтобы не утонуть в теории, начнём с малого — с первой корутины.

import asyncio

async def greet():
print("Привет")
await asyncio.sleep(1)
print("Мир")

asyncio.run(greet())

Слово async перед def говорит: эта функция — корутина. Она не выполняется сразу при вызове greet(). Вызов возвращает объект-корутину, который нужно «запустить» внутри цикла событий. asyncio.run() — самый простой способ для скриптов и учебных примеров: создаёт цикл, выполняет переданную корутину до конца, закрывает цикл и возвращает результат.

Цикл событий — сердце asyncio. Представьте диспетчера на перекрёстке: он не водит машины сам, но знает, кто где стоит, кому можно проехать, кого отпустить на секунду и кого вернуть в очередь. Когда корутина доходит до await, она говорит циклу: «Я жду. Займись другими». Цикл переключается на другую готовую работу. Когда ожидание закончилось, корутина снова в очереди.

await asyncio.sleep(1) — не time.sleep(1). Обычный sleep блокирует весь поток: никто больше ничего не делает секунду. Асинхронный sleep отдаёт управление циклу. За эту секунду могут выполниться сотни других корутин. Именно здесь рождается неблокирующее выполнение: блокировки нет на уровне потока, есть кооперативная передача управления в точках await.

Артём попробовал на практике. Два запроса к медленному API синхронно — четыре секунды. Два асинхронных — чуть больше двух, потому что ожидания шли параллельно. Разница не магия, а арифметика ожидания.

import asyncio

async def fetch_slow(name, delay):
print(f"{name}: начал")
await asyncio.sleep(delay)
print(f"{name}: готов")
return f"данные-{name}"

async def main():
a = await fetch_slow("A", 2)
b = await fetch_slow("B", 2)
print(a, b)

asyncio.run(main())

Здесь B начнётся только после A — последовательное выполнение. Чтобы запустить одновременно, нужны задачи.

async def main():
task_a = asyncio.create_task(fetch_slow("A", 2))
task_b = asyncio.create_task(fetch_slow("B", 2))
a = await task_a
b = await task_b
print(a, b)

create_task() планирует корутину на выполнение немедленно и возвращает объект Task. Task — обёртка над корутиной с состоянием: pending, running, done. Пока main ждёт task_a, цикл уже крутит task_b. Обе «спят» параллельно, и общее время близко к двум секундам, а не четырём.

На собеседовании часто спрашивают: чем Task отличается от простого await coro()? Разница во времени старта. await coro() запускает корутину только в момент await. create_task() ставит её в очередь сразу — «fire and forget» с возможностью потом забрать результат.

Есть и asyncio.gather() — удобно, когда нужно дождаться группы корутин и получить список результатов в том же порядке:

async def main():
results = await asyncio.gather(
fetch_slow("A", 2),
fetch_slow("B", 2),
fetch_slow("C", 1),
)
print(results)

gather принимает awaitable-объекты, планирует их и ждёт все сразу. Если одна упадёт с исключением, по умолчанию gather тоже упадёт — это важно помнить при проектировании.

Другой паттерн — asyncio.wait(), когда нужен контроль: ждать все, ждать первую завершившуюся, обрабатывать по мере готовности. Для «кто первый ответил — того и берём» — asyncio.wait() с return_when=FIRST_COMPLETED. Отдельно есть asyncio.as_completed():

async def main():
tasks = [
asyncio.create_task(fetch_slow("A", 3)),
asyncio.create_task(fetch_slow("B", 1)),
asyncio.create_task(fetch_slow("C", 2)),
]
for aw in asyncio.as_completed(tasks):
result = await aw
print("Готов:", result)

as_completed() возвращает итератор awaitable-объектов, которые завершаются по мере готовности, — удобно для стриминга ответов или отмены лишних задач, когда первый результат уже получен.

Мидл должен понимать и asyncio.run(), и низкоуровневую работу с циклом. run() внутри делает примерно следующее: получает или создаёт цикл, run_until_complete(main()), закрывает. В существующем приложении — например, внутри FastAPI или уже запущенного uvicorn — цикл уже есть, и asyncio.run() вызывать нельзя: будет ошибка «event loop is already running». Там используют get_running_loop() и создают задачи из уже работающего контекста. get_event_loop() оставляют для старого синхронного bootstrap-кода; внутри работающего приложения он легко введёт в заблуждение.

Иногда в тестах или интеграции со старым синхронным кодом создают новый цикл вручную:

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main())
finally:
loop.close()



Отредактировано: 20.07.2026





Понравилась книга?
Отложите ее в библиотеку, чтобы не потерять