- Объяснять понятия корутины,
awaitи цикла событий - Запускать корутины конкурентно с помощью
gather,create_taskиTaskGroup - Задавать тайм-ауты через
wait_forи использоватьasync for - Понимать, когда asyncio полезен и что блокирует цикл событий
Программе нужно скачать из интернета 100 страниц. Каждый запрос около 0,3 с просто ждёт сеть — процессор в это время ничего не делает. Если выполнять запросы по очереди, получится 30 секунд почти пустого ожидания. Вспомни официанта в чайхане: он не стоит у двери кухни, пока готовится один заказ, а тем временем принимает заказ у следующего столика. Асинхронное программирование позволяет одному потоку работать так же: пока одна операция ждёт, другие продвигаются.
Корутины, await и цикл событий
Функция, определённая через async def, — это корутинная функция. Её вызов не выполняет тело: как и функция-генератор, она возвращает объект корутины, который нужно дождаться через await.
await выполняет корутину и ждёт её результата. На каждом await, где операция ещё не завершена, корутина приостанавливается и возвращает управление циклу событий (event loop) — планировщику, который решает, какая из приостановленных корутин продолжит работу. Ничего не выполняется параллельно: поток один, а корутины добровольно уступают друг другу очередь. Это называется кооперативной многозадачностью.
import asyncio
async def greet(name, delay):
await asyncio.sleep(delay)
print(f'Hello, {name}!')
return name.upper()
coro = greet('Aysel', 0.1)
print(type(coro).__name__)
result = await coro
print(result)▸ Ожидаемый результат
coroutine Hello, Aysel! AYSEL
await можно писать прямо на верхнем уровне.В обычном файле .py await вне функции — синтаксическая ошибка. Там у программы есть одна входная корутина, обычно main(), а цикл событий запускается один раз, в самом конце скрипта, вызовом **asyncio.run(main())**:
import asyncio
async def fetch(name, seconds):
await asyncio.sleep(seconds)
return f'{name} ({seconds} s)'
async def main():
results = await asyncio.gather(fetch('rates', 0.3), fetch('news', 0.1))
print(results)
if __name__ == '__main__':
asyncio.run(main())['rates (0.3 s)', 'news (0.1 s)']
python app.py.Конкурентный запуск через gather
asyncio.gather(*coroutines) запускает несколько корутин сразу и ждёт, пока все они завершатся. Посмотри на порядок строк: все три стартуют немедленно и заканчивают в порядке времени ожидания, а не в порядке записи. А вот результаты возвращаются в порядке аргументов:
import asyncio
async def fetch(name, seconds):
print(f'start {name}')
await asyncio.sleep(seconds)
print(f'done {name}')
return f'{name} ({seconds} s)'
results = await asyncio.gather(
fetch('rates', 0.3),
fetch('weather', 0.2),
fetch('news', 0.1),
)
print(results)▸ Ожидаемый результат
start rates start weather start news done news done weather done rates ['rates (0.3 s)', 'weather (0.2 s)', 'news (0.1 s)']
Теперь измерим. Если дожидаться корутин по очереди, ожидания складываются; с gather они накладываются друг на друга, поэтому общее время равно самому долгому ожиданию:
import asyncio
import time
async def fetch(seconds):
await asyncio.sleep(seconds)
start = time.perf_counter()
for s in (0.3, 0.2, 0.1):
await fetch(s)
print(f'one after another: {time.perf_counter() - start:.2f} s')
start = time.perf_counter()
await asyncio.gather(fetch(0.3), fetch(0.2), fetch(0.1))
print(f'with gather: {time.perf_counter() - start:.2f} s')Задачи, TaskGroup и тайм-ауты
asyncio.create_task(coro) сразу планирует выполнение корутины в фоне и возвращает задачу (task). Тем временем можно заниматься другой работой, а результат получить позже через await задачи. Начиная с Python 3.11 **asyncio.TaskGroup** даёт структурированную конкурентность: блок async with не завершается, пока не закончатся все его задачи, а если одна из них падает, остальные автоматически отменяются:
import asyncio
async def worker(name, seconds):
await asyncio.sleep(seconds)
print(f'{name} finished')
return seconds
async def main():
task = asyncio.create_task(worker('backup', 0.2))
print('task created, doing other work...')
await asyncio.sleep(0.1)
print('other work done, task done?', task.done())
print('result:', await task)
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(worker('A', 0.2))
t2 = tg.create_task(worker('B', 0.1))
print('group results:', t1.result(), t2.result())
await main()▸ Ожидаемый результат
task created, doing other work... other work done, task done? False backup finished result: 0.2 B finished A finished group results: 0.2 0.1
Сетевые сервисы иногда вообще не отвечают. asyncio.wait_for(coro, timeout) отменяет операцию через заданное число секунд и возбуждает TimeoutError (начиная с Python 3.11 asyncio.TimeoutError — тот же класс):
import asyncio
async def slow_api():
await asyncio.sleep(1)
return 'data'
try:
result = await asyncio.wait_for(slow_api(), timeout=0.2)
except TimeoutError:
print('the API did not answer in 0.2 s')▸ Ожидаемый результат
the API did not answer in 0.2 s
У других конструкций языка тоже есть асинхронные версии: async with — для контекстных менеджеров, подготовка или очистка которых требует ожидания (например, сетевое соединение), и async for — для асинхронных итераторов. Асинхронный генератор — это функция async def, в которой есть yield:
import asyncio
async def ticker(n, delay):
for i in range(1, n + 1):
await asyncio.sleep(delay)
yield i
async for value in ticker(3, 0.05):
print('tick', value)
squares = [v * v async for v in ticker(4, 0.01)]
print(squares)▸ Ожидаемый результат
tick 1 tick 2 tick 3 [1, 4, 9, 16]
Ловушки и когда выбирать asyncio
| Подход | Для чего подходит | Пример |
|---|---|---|
asyncio | много одновременных ожиданий в одном потоке (сеть, сокеты) | чат-сервер, веб-краулер, FastAPI |
потоки (threading) | блокирующие библиотеки, которые нельзя ждать через await | несколько загрузок через requests |
процессы (multiprocessing) | тяжёлые вычисления на всех ядрах процессора | обработка изображений, моделирование |
Приготовь три блюда одновременно с помощью asyncio.gather и выведи список результатов. Обрати внимание: строки «ready» появляются в порядке времени готовки, а список сохраняет порядок меню.
import asyncio
async def cook(dish, seconds):
await asyncio.sleep(seconds)
print('ready:', dish)
return dish
menu = [('plov', 0.3), ('salad', 0.1), ('soup', 0.2)]
# cook all dishes at the same time with asyncio.gather and print the results▸ Ожидаемый результат
ready: salad ready: soup ready: plov ['plov', 'salad', 'soup']
Напиши корутину fetch_with_limit: она запускает fetch с ограничением времени limit секунд и возвращает результат, а если время вышло — строку '<name>: timeout'.
import asyncio
async def fetch(name, seconds):
await asyncio.sleep(seconds)
return f'{name}: ok'
async def fetch_with_limit(name, seconds, limit):
# return fetch's result, or f'{name}: timeout' if it takes longer than limit
...
results = await asyncio.gather(
fetch_with_limit('fast', 0.05, 0.2),
fetch_with_limit('slow', 0.5, 0.2),
)
print(results)▸ Ожидаемый результат
['fast: ok', 'slow: timeout']
Главное
async defопределяет корутину; её вызов возвращает объект корутины, аawaitвыполняет его.- На каждом
awaitкорутина приостанавливается, и цикл событий запускает другую — один поток, кооперативная многозадачность. - Скрипт запускается через
asyncio.run(main()); в браузере и Jupyter пишутawaitнапрямую. gather,create_taskиTaskGroupвыполняют корутины конкурентно; общее время ≈ самое долгое ожидание.- Не блокируй цикл через
time.sleepили тяжёлые вычисления; asyncio — для задач с ожиданием ввода-вывода.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
async def load(): .... Что возвращает вызов load()?