使用asyncio时,如何在关闭事件循环之前完成所有正在运行的任务

时间:2015-01-06 10:06:31

标签: python python-3.4 python-asyncio

我有以下代码:

@asyncio.coroutine
def do_something_periodically():
    while True:
        asyncio.async(my_expensive_operation())
        yield from asyncio.sleep(my_interval)
        if shutdown_flag_is_set:
            print("Shutting down")
            break

我运行此功能直到完成。设置关闭时会出现问题 - 该功能完成,任何挂起的任务都不会运行。 (你认为这是一个错误

task: <Task pending coro=<report() running at script.py:33> wait_for=<Future pending cb=[Task._wakeup()]>>

)。如何正确安排关机?

为了给出一些上下文,我写了一个系统监视器,它每隔5秒从/ proc / stat读取一次,计算该时间段内的CPU使用率,然后将结果发送到服务器。我希望继续安排这些监视作业,直到我收到sigterm,当我停止计划,等待所有当前作业完成,然后正常退出。

7 个答案:

答案 0 :(得分:41)

您可以检索未完成的任务并再次运行循环,直到完成,然后关闭循环或退出程序。

pending = asyncio.Task.all_tasks()
loop.run_until_complete(asyncio.gather(*pending))
  • pending是待处理任务的列表。
  • asyncio.gather()允许一次等待几个任务。

如果你想确保所有任务都在一个协程中完成(也许你有一个&#34; main&#34; coroutine),你可以这样做,例如:

@asyncio.coroutine
def do_something_periodically():
    while True:
        asyncio.async(my_expensive_operation())
        yield from asyncio.sleep(my_interval)
        if shutdown_flag_is_set:
            print("Shutting down")
            break

    yield from asyncio.gather(*asyncio.Task.all_tasks())

此外,在这种情况下,由于所有任务都是在同一个协同程序中创建的,因此您已经可以访问这些任务:

@asyncio.coroutine
def do_something_periodically():
    tasks = []
    while True:
        tasks.append(asyncio.async(my_expensive_operation()))
        yield from asyncio.sleep(my_interval)
        if shutdown_flag_is_set:
            print("Shutting down")
            break

    yield from asyncio.gather(*tasks)

答案 1 :(得分:7)

从Python 3.7开始,以上答案使用了多个弃用的API (asyncio.async和Task.all_tasks,@ asyncio.coroutine,收益率等),您应该使用以下方法:

import asyncio


async def my_expensive_operation(expense):
    print(await asyncio.sleep(expense, result="Expensive operation finished."))


async def do_something_periodically(expense, interval):
    while True:
        asyncio.create_task(my_expensive_operation(expense))
        await asyncio.sleep(interval)


loop = asyncio.get_event_loop()
coro = do_something_periodically(1, 1)

try:
    loop.run_until_complete(coro)
except KeyboardInterrupt:
    coro.close()
    tasks = asyncio.all_tasks(loop)
    expensive_tasks = {task for task in tasks if task._coro.__name__ != coro.__name__}
    loop.run_until_complete(asyncio.gather(*expensive_tasks))

答案 2 :(得分:2)

我不确定这是否是您要的,但是我遇到了类似的问题,这是我想出的最终解决方案。

该代码与python 3兼容,并且仅使用公共asyncio API(意味着没有hacky _coro,也没有弃用的API)。

import asyncio

async def fn():
  await asyncio.sleep(1.5)
  print('fn')

async def main():
    print('main start')
    asyncio.create_task(fn()) # run in parallel
    await asyncio.sleep(0.2)
    print('main end')


def async_run_and_await_all_tasks(main):
  def get_pending_tasks():
      tasks = asyncio.Task.all_tasks()
      pending = [task for task in tasks if task != run_main_task and not task.done()]
      return pending

  async def run_main():
      await main()

      while True:
          pending_tasks = get_pending_tasks()
          if len(pending_tasks) == 0: return
          await asyncio.gather(*pending_tasks)

  loop = asyncio.new_event_loop()
  run_main_coro = run_main()
  run_main_task = loop.create_task(run_main_coro)
  loop.run_until_complete(run_main_task)

# asyncio.run(main()) # doesn't print from fn task, because main finishes earlier
async_run_and_await_all_tasks(main)

输出(按预期):

main start
main end
fn

该async_run_and_await_all_tasks函数将使python以nodejs的方式运行:仅在没有未完成的任务时退出。

答案 3 :(得分:0)

您还可以考虑使用asyncio.shield,但通过这样做,你不会得到的所有的正在运行的任务完成了,但只有屏蔽的。但它仍然可以在某些情况下是有用的。

此外,从Python 3.7开始,我们还可以在此处使用高级API方法asynio.run。作为Python核心开发人员,Yury Selivanov建议: https://youtu.be/ReXxO_azV-w?t=636
注意:asyncio.run函数已临时添加到Python 3.7中的asyncio中。

希望有帮助!

import asyncio


async def my_expensive_operation(expense):
    print(await asyncio.sleep(expense, result="Expensive operation finished."))


async def do_something_periodically(expense, interval):
    while True:
        asyncio.create_task(my_expensive_operation(expense))
        # using asyncio.shield
        await asyncio.shield(asyncio.sleep(interval))


coro = do_something_periodically(1, 1)

if __name__ == "__main__":
    try:
        # using asyncio.run
        asyncio.run(coro)
    except KeyboardInterrupt:
        print('Cancelled!')

答案 4 :(得分:0)

使用包装协程,该协程等待直到挂起的任务计数为1才返回。

async def loop_job():
    asyncio.create_task(do_something_periodically())
    while len(asyncio.Task.all_tasks()) > 1:  # Any task besides loop_job() itself?
        await asyncio.sleep(0.2)

asyncio.run(loop_job())

答案 5 :(得分:0)

如果您想要一种干净的方式来等待在某个本地范围内创建的所有正在运行的任务而不会泄漏内存(并同时防止 garbage collection errors),您可以维护一组正在运行的任务并使用 task.add_done_callback(...)从集合中删除任务。这是一个为您处理此问题的类:

class TaskSet:
    def __init__(self):
        self.tasks = set()

    def add(self, coroutine: Coroutine) -> Task:
        task = asyncio.create_task(coroutine)
        self.tasks.add(task)
        task.add_done_callback(lambda _: self.tasks.remove(task))
        return task

    def __await__(self):
        return asyncio.gather(*self.tasks).__await__()

可以这样使用:

async def my_function():
    await asyncio.sleep(0.5)


async def go():
    tasks = TaskSet()
    for i in range(10):
        tasks.add(my_function())
    await tasks

答案 6 :(得分:0)

我注意到一些建议使用 asyncio.gather(*asyncio.all_tasks()) 的答案,但问题有时可能是无限循环,它等待 asyncio.current_task() 完成,这就是它本身。一些答案提出了一些复杂的解决方法,包括检查 coro 名称或 len(asyncio.all_tasks()),但事实证明,利用 set 操作非常简单:

async def main():
    # Create some tasks.
    for _ in range(10):
        asyncio.create_task(asyncio.sleep(10))
    # Wait for all other tasks to finish other than the current task i.e. main().
    await asyncio.gather(*asyncio.all_tasks() - {asyncio.current_task()})