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

#python #python-3.x #python-asyncio

Вопрос:

Каков стандартный способ в Python гарантировать, что все параллельные задачи будут выполнены до завершения цикла событий? Вот упрощенный пример:

 import asyncio

async def foo(delay):
    print("Start foo.") # Eg: Send message
    asyncio.create_task(bar(delay))
    print("End foo.")

async def bar(delay):
    print("Start bar.")
    await asyncio.sleep(delay)
    print("End bar.") # Eg: Delete message after delay

def main():
    asyncio.run(foo(2))

if __name__ == "__main__":
    main()
 

Выходной ток:

 Start foo. # Eg: Send message
End foo.
Start bar.
 

Желаемый результат:

 Start foo. # Eg: Send message
End foo.
Start bar.
End bar. # Eg: Delete message after delay
 

Я попытался запустить все оставшиеся задачи после loop.run_until_complete() этого , но это не сработает, так как к тому времени цикл будет завершен. Я также попытался изменить основную функцию следующим образом:

 async def main():
    await foo(2)

    tasks = asyncio.all_tasks()
    if len(tasks) > 0:
        await asyncio.wait(tasks)

if __name__ == "__main__":
    asyncio.run(main())
 

Вывод правильный, но он никогда не завершается, так как сопрограмма main() является одной из задач. Описанная выше настройка также описывает, как discord.py отправляет сообщение и удаляет его через определенный промежуток времени, за исключением того, что оно используется loop.run_forever() вместо этого, поэтому проблема не возникает.

Комментарии:

1. Попробуйте: task = [asyncio.create_task(bar(delay))] а затем await asyncio.gather(*task, return_exceptions=False) в вашей функции foo.

2. Обратите внимание, что в эти дни вы не должны напрямую взаимодействовать с циклом. Используйте asyncio.run вместо получения и запуска цикла вручную.

3. @user56700 Я попробовал, но он блокируется foo до тех пор, пока панель не завершится. bar должно выполняться одновременно. Примером может служить бот, отправляющий сообщение с истекшим сроком действия. Функция отправки сообщения должна выполняться как можно быстрее и возвращаться (вызывающая функция хочет только дождаться отправки сообщения, а не истечения срока действия сообщения).

4. @MisterMiyagi Спасибо, я отредактировал сообщение с улучшенным кодом.

Ответ №1:

Не существует стандартного способа дождаться всех задач в asyncio (и аналогичных фреймворках), и на самом деле не следует пытаться. Говоря в терминах потоков, a Task выражает как регулярные, так и демонические действия. Ожидание всех задач без разбора может привести к остановке приложения на неопределенный срок.

Задача, которая создается, но никогда await не редактируется, де-факто является фоновой/демонической задачей. Напротив, если задача не должна рассматриваться как фоновая/демоническая, то ответственность за ее выполнение лежит на вызывающих абонентах await .


Самое простое решение-для каждой сопрограммы await отменить и/или отменить все задачи, которые она порождает.

 async def foo(delay):
    print("Start foo.")
    task = asyncio.create_task(bar(delay))
    print("End foo.")
    await task  # foo is done here, it ensures the other task finishes as well
 

Поскольку весь смысл async /задач заключается в дешевом переключении задач, это дешевая операция. Это также не должно влиять на какие-либо хорошо продуманные приложения:

  • Если целью функции является создание значения, любые дочерние задачи должны быть частью создания этого значения.
  • Если целью функции является какой-либо побочный эффект, любые дочерние задачи должны быть частями этого побочного эффекта.

Для более сложных ситуаций может быть целесообразно вернуть любые невыполненные задачи.

 async def foo(delay):
    print("Start foo.")
    task = asyncio.create_task(bar(delay))
    print("End foo.")
    return task  # allow the caller to wait for our child tasks
 

Это требует от вызывающего абонента явной обработки нерешенных задач, но дает быстрые ответы и максимальный контроль. Задача верхнего уровня затем отвечает за обработку любых сиротских задач.

Для async программирования в целом парадигма структурированного программирования кодирует идею «обработки нерешенных задач» в объекте управления. В Python этот шаблон был закодирован trio библиотекой в виде так называемых Nursery объектов.

 import trio

async def foo(delay, nursery):
    print("Start foo.")
    # spawning a task via a nursery means *someone* awaits it
    nursery.start_soon(bar, delay)
    print("End foo.")

async def bar(delay):
    print("Start bar.")
    await trio.sleep(delay)
    print("End bar.")

async def main():
    # a task may spawn a nursery and pass it to child tasks
    async with trio.open_nursery() as nursery:
        await foo(2, nursery)

if __name__ == "__main__":
    trio.run(main)
 

Хотя эта модель была предложена для asyncio as TaskGroups , до сих пор она была отложена.
Однако различные порты шаблона для asyncio доступны через сторонние библиотеки.