Аналогом генераторов в asyncio будет корутина (coroutine) - функция, выполнение которой можно приостановить и затем вновь запустить. Как видим, под определение корутины полностью подходит и генератор. За одним исключением: корутина обладает свойством awaitable - способностью (возможностью) быть использована с оператором await.
Для запуска корутин нам потребуется Цикл Событий (Event Loop) - ключевой элемент в asyncio, который запускает асинхронные задачи. В пакете asyncio цикл событий представлен достаточно широким инструментарием различный низкоуровневых методов и функций. Однако важно помнить, что для реализации большинства практических задач более чем достаточно использование высокоуровневой функции asyncio.run().
https://docs.python.org/3/library/asyncio-eventloop.html#event-loop
Давайте рассмотрим простейший пример:
import asyncio
async def cor1():
print('cor1')
asyncio.run(cor1())
Все попытки запустить корутину cor1() как обычную функцию приведут к ошибке: RuntimeWarning: coroutine 'cor1' was never awaited. Что говорит о том, запустить корутину означает сообщить циклу событий о том, что корутина поставлена в ожидание запуска с помощью оператора await. Иными словами, конструкций await cor1() мы, как бы, сообщаем циклу событий примерно следующее: “Мы готовы запустить корутину cor1() в этом месте программы. Пожалуйста, сделай это при первой возможности”.
В нашем предыдущем примере функция syncio.run() уже содержит в себе этот оператор неявно, поэтому код запускается без ошибки. Однако мы можем запустит корутину явно из другой корутины:
import asyncio
async def cor1():
print('cor1')
async def main():
await cor1()
print('The cor1 coroutine has been started.')
asyncio.run(main())
Давайте усложним код и попробуем запустить две корутины параллельно. Для этого в каждой из корутин мы установим разную задержку и посмотрим, насколько успешно asyncio справится с этой задачей.
Кстати, обратите внимание, что задержка в нашем примере берётся не из пакета time, а из пакет asyncio. Объяснение тут следующее: в принципе, в корутине мы можем применять любые функции. Однако, если мы хотим избежать блокировки внутри корутины, эта функция должно обладать свойством awaitable (“Быть awaitable“ - это корректное определение. К сожалению, гораздо чаще можно услышать “функция должна быть асинхронной”)
import time
import asyncio
from my_deco import async_time_counter
async def cor1(start):
print(f'cor1 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(1)
print(f'cor1 after with delay: {time.time() - start:0.0f}')
async def cor2(start):
print(f'cor2 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(2)
print(f'cor2 after with delay: {time.time() - start:0.0f}')
@async_time_counter
async def main():
start = time.time()
await cor1(start)
await cor2(start)
print(f'and all with delay: {time.time() - start:0.0f}')
asyncio.run(main())
Как видим, фокус не удался - корутины выполнились последовательно, а не параллельно:
======== Script started ========
cor1 before with delay: 0
cor1 after with delay: 1
cor2 before with delay: 1
cor2 after with delay: 3
and all with delay: 3
======== Script execution time: 3.00 ========
Это произошло от того, что для параллельного выполнения корутин мало того, что всё функции внутри корутин были бы awaitable-объектами. Корутины необходимо передать в цикл событий не как корутины, а как задачи. Сделать это можно несколькими способами.
Во-первых, это можно сделать явно, превратив корутину в задачу с помощью функции asyncio.create_task() :
import time
import asyncio
from my_deco import async_time_counter
async def cor1(start):
print(f'cor1 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(1)
print(f'cor1 after with delay: {time.time() - start:0.0f}')
async def cor2(start):
print(f'cor2 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(2)
print(f'cor2 after with delay: {time.time() - start:0.0f}')
@async_time_counter
async def main():
start = time.time()
task1 = asyncio.create_task(cor1(start))
task2 = asyncio.create_task(cor2(start))
await task1
await task2
print(f'and all with delay: {time.time() - start:0.0f}')
asyncio.run(main())
Как видите, нас можно поздравить - теперь общее время выполнения скрипта сократилось до времени выполнения самой “долгой” корутины:
======== Script started ========
cor1 before with delay: 0
cor2 before with delay: 0
cor1 after with delay: 1
cor2 after with delay: 2
and all with delay: 2
======== Script execution time: 2.00 ========
Во-вторых, ровно тоже самое можно сделать и неявно, с помощью замечательной функции asyncio.gather(), которая автоматически превратит в задачи все корутины, переданные в неё как аргументы:
import time
import asyncio
from my_deco import async_time_counter
async def cor1(start):
print(f'cor1 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(5)
print(f'cor1 after with delay: {time.time() - start:0.0f}')
async def cor2(start):
print(f'cor2 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(2)
print(f'cor2 after with delay: {time.time() - start:0.0f}')
@async_time_counter
async def main():
start = time.time()
"""
From tutorial:
awaitable asyncio.gather(*aws, return_exceptions=False)
"If any awaitable in aws is a coroutine, it is automatically scheduled as a Task."
https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
"""
await asyncio.gather(cor1(start), cor2(start))
print(f'and all with delay: {time.time() - start:0.0f}')
asyncio.run(main())
И, в-третьих, в Python версии 3.11 появилась возможность выполнить превращение корутин в задачи ещё более изящно - с помощью asyncio.TaskGroup():
import time
import asyncio
from my_deco import async_time_counter
async def cor1(start):
print(f'cor1 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(5)
print(f'cor1 after with delay: {time.time() - start:0.0f}')
async def cor2(start):
print(f'cor2 before with delay: {time.time() - start:0.0f}')
await asyncio.sleep(2)
print(f'cor2 after with delay: {time.time() - start:0.0f}')
@async_time_counter
async def main():
start = time.time()
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(cor1(start))
task2 = tg.create_task(cor2(start))
print(f'and all with delay: {time.time() - start:0.0f}')
asyncio.run(main())
Таким образом, мы выяснили, что третьим важнейшим элементом пакета asyncio является Задача (Task). Именно задачи позволяют выполняются корутинам параллельно, точнее, квази-параллельно (concurrently).
https://docs.python.org/3/library/asyncio-task.html#:~:text=a%20coroutine%20function.-,Tasks,%23%20with%20%22main().
Более подробно со всеми деталями этой темы вы сможете познакомиться из этого видео: