asyncio: корутины и цикл событий
Идея: не ждать впустую
Сервер получает запрос и идёт в базу. Пока база отвечает 50 мс, процессор свободен. Асинхронность позволяет в это время обслужить другие запросы — в одном потоке, переключаясь между задачами в точках ожидания.
import asyncio, time
async def make_coffee(name, seconds):
print(f" {name}: начал")
await asyncio.sleep(seconds) # «ждём» и отдаём управление другим
print(f" {name}: готово")
return name
async def main():
start = time.perf_counter()
results = await asyncio.gather(
make_coffee("эспрессо", 0.3),
make_coffee("капучино", 0.5),
make_coffee("латте", 0.4),
)
print(results, f"за {time.perf_counter() - start:.1f} с") # ≈ 0.5, а не 1.2
asyncio.run(main())
async def и await
async defобъявляет корутинную функцию. Её вызов ничего не выполняет, а возвращает объект-корутину.awaitвыполняет корутину (или другой awaitable — задачу, Future) и приостанавливает текущую, пока результат не готов.awaitможно писать только внутриasync def.asyncio.run(main())создаёт цикл событий, выполняетmainдо конца и закрывает цикл. Это точка входа.
import asyncio, inspect
async def add(a, b):
await asyncio.sleep(0)
return a + b
coro = add(2, 3)
print(coro, inspect.iscoroutine(coro))
print(asyncio.run(coro)) # выполнить
async def main():
x = await add(1, 2) # внутри корутины — через await
y = await add(x, 10)
return y
print(asyncio.run(main()))
Цикл событий
Многозадачность кооперативная: задача сама отдаёт управление на await. Пока корутина выполняет обычный код без await, никто другой не работает.
Задачи: create_task и gather
Сам по себе await coro() — последовательное выполнение. Чтобы корутины шли конкурентно, их оборачивают в задачи (Task) — задача сразу ставится в цикл событий:
import asyncio, time
async def job(name, delay):
await asyncio.sleep(delay)
return f"{name} ({delay} с)"
async def main():
start = time.perf_counter()
print(await job("A", 0.3), await job("B", 0.3)) # последовательно
print(f" {time.perf_counter() - start:.1f} с")
start = time.perf_counter()
task_a = asyncio.create_task(job("A", 0.3)) # уже запущены
task_b = asyncio.create_task(job("B", 0.3))
print(await task_a, await task_b)
print(f" {time.perf_counter() - start:.1f} с")
start = time.perf_counter()
print(await asyncio.gather(job("A", 0.3), job("B", 0.3), job("C", 0.3)))
print(f" {time.perf_counter() - start:.1f} с")
asyncio.run(main())
Пройдите пример 👣 Пошагово — видно, как выполнение переключается между корутинами на каждом await.
Не теряйте задачи
Цикл событий хранит только слабые ссылки на задачи. Задачу, созданную через create_task и нигде не сохранённую, может удалить сборщик мусора посреди работы. Сохраняйте ссылки или используйте TaskGroup (следующий урок).
Ловушка: блокирующий код
import asyncio, time
async def bad_worker(name):
time.sleep(0.3) # ✗ блокирует весь поток — и цикл событий
return name
async def good_worker(name):
await asyncio.sleep(0.3) # ✓ отдаёт управление
return name
async def main():
start = time.perf_counter()
await asyncio.gather(*(bad_worker(i) for i in range(3)))
print(f"time.sleep: {time.perf_counter() - start:.1f} с")
start = time.perf_counter()
await asyncio.gather(*(good_worker(i) for i in range(3)))
print(f"await asyncio.sleep: {time.perf_counter() - start:.1f} с")
asyncio.run(main())
Блокирующие вызовы в корутинах: time.sleep, requests.get, синхронные драйверы БД (psycopg2), чтение больших файлов, тяжёлые вычисления. Что делать:
| Вместо | Используйте |
|---|---|
time.sleep |
await asyncio.sleep |
requests |
httpx.AsyncClient, aiohttp |
psycopg2, синхронный SQLAlchemy |
asyncpg, psycopg (async), SQLAlchemy AsyncSession |
open().read() больших файлов |
aiofiles или asyncio.to_thread |
| синхронная библиотека без async-версии | await asyncio.to_thread(func, *args) — в отдельном потоке |
| тяжёлые вычисления | loop.run_in_executor(ProcessPoolExecutor(), ...) |
async with и async for
import asyncio
class Connection:
async def __aenter__(self): # асинхронный контекстный менеджер
await asyncio.sleep(0.01)
print(" подключились")
return self
async def __aexit__(self, *exc):
await asyncio.sleep(0.01)
print(" отключились")
async def rows(self): # асинхронный генератор
for i in range(3):
await asyncio.sleep(0.01) # «следующая порция из БД»
yield {"id": i}
async def main():
async with Connection() as conn:
async for row in conn.rows():
print(" ", row)
print([r["id"] async for r in conn.rows()]) # асинхронное включение
asyncio.run(main())
Так выглядит работа с реальными библиотеками:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/users")
async with async_session() as session:
result = await session.execute(select(User))
Как это устроено
Корутины выросли из генераторов: await работает почти как yield from. Корутина «отдаёт» наружу объект Future («результат будет позже»), цикл событий запоминает, кого разбудить, когда Future получит результат, и продолжает корутину через send():
import asyncio
async def child():
await asyncio.sleep(0) # здесь корутина уступает управление
return 42
coro = child()
print(coro.send(None)) # 1-й шаг: корутина «отдала» управление (yield внутри sleep(0))
try:
coro.send(None) # 2-й шаг: продолжили — корутина завершилась
except StopIteration as stop:
print("результат:", stop.value) # return корутины = StopIteration.value
Цикл событий делает то же самое, но для тысяч корутин, и будит их по готовности сокетов и таймеров.
Вопрос на собеседовании
«Чем asyncio отличается от потоков?» — asyncio работает в одном потоке с кооперативной многозадачностью: задачи переключаются только на await, поэтому нет гонок между двумя await и нет затрат на потоки — можно держать десятки тысяч соединений. Но любой блокирующий вызов останавливает всех, и нужны асинхронные библиотеки. Потоки переключает ОС в любой момент, они работают с обычными блокирующими библиотеками, но дороже и требуют блокировок.
Итог
async defсоздаёт корутину;awaitждёт результат, уступая управление;asyncio.run— точка входа.- Цикл событий в одном потоке переключает задачи на
await(кооперативная многозадачность). awaitподряд — последовательно; конкурентно —create_task,gather,TaskGroup.- Блокирующие вызовы останавливают весь цикл: используйте async-библиотеки или
asyncio.to_thread. async with,async for, асинхронные генераторы — для асинхронных ресурсов и потоков данных.
Практика решено 0 из 5
Напишите корутину fetch_all(delays): для каждого числа из delays запустите корутину fake_fetch(i, d)
(уже написана — «ждёт» d секунд и возвращает i) конкурентно через asyncio.gather
и верните список результатов в исходном порядке. Общее время должно быть ≈ максимальной задержке,
а не сумме.
Какие тесты проверяют код (2)
import asyncio; assert asyncio.run(fetch_all([0.05, 0.01, 0.03])) == [0, 1, 2]
import asyncio, time
start = time.perf_counter()
asyncio.run(fetch_all([0.2, 0.2, 0.2, 0.2]))
elapsed = time.perf_counter() - start
assert elapsed < 0.5, f"Задачи выполнялись последовательно: {elapsed:.2f} с вместо ≈0.2 с"
import asyncio
async def fake_fetch(i, delay):
await asyncio.sleep(delay)
return i
async def fetch_all(delays):
return await asyncio.gather(*(fake_fetch(i, d) for i, d in enumerate(delays)))
Напишите асинхронный генератор ticker(n, delay), который выдаёт числа 0..n-1,
делая await asyncio.sleep(delay) перед каждым. И корутину collect(n), которая
собирает все значения ticker(n, 0.001) через async for и возвращает список.
Какие тесты проверяют код (2)
import asyncio; assert asyncio.run(collect(5)) == [0, 1, 2, 3, 4] import inspect; assert inspect.isasyncgenfunction(ticker), "ticker должна быть асинхронным генератором (async def + yield)"
import asyncio
async def ticker(n, delay):
for i in range(n):
await asyncio.sleep(delay)
yield i
async def collect(n):
return [x async for x in ticker(n, 0.001)]
Что выведет код?
async def hello():
return "привет"
print(hello())
В асинхронном обработчике FastAPI вызвали time.sleep(5). Что произойдёт?
Будут ли корутины выполняться конкурентно?
| да | нет, последовательно | |
|---|---|---|
await a(); await b() |
||
await asyncio.gather(a(), b()) |
||
t1 = asyncio.create_task(a()); t2 = asyncio.create_task(b()); await t1; await t2 |
||
async with asyncio.TaskGroup() as tg: + tg.create_task(a()), tg.create_task(b()) |
||
for c in [a(), b()]: await c |
Войдите, чтобы отмечать пройденные уроки.