asyncio.ensure_future vs. BaseEventLoop.create_task vs. simple coroutine? asyncio.ensure_future、BaseEventLoop.create_task和简单协程有什么区别?

136

我看过几个基础的 Python 3.5 asyncio 教程,它们都用不同的方式执行相同的操作。

在这段代码中:

import asyncio  

async def doit(i):
    print("Start %d" % i)
    await asyncio.sleep(3)
    print("End %d" % i)
    return i

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    #futures = [asyncio.ensure_future(doit(i), loop=loop) for i in range(10)]
    #futures = [loop.create_task(doit(i)) for i in range(10)]
    futures = [doit(i) for i in range(10)]
    result = loop.run_until_complete(asyncio.gather(*futures))
    print(result)

上述定义futures变量的三种变体都可以实现相同的结果;我唯一看到的区别是,第三种变体的执行顺序是无序的(在大多数情况下不应该有影响)。是否还有其他区别?是否存在不能仅使用最简单的变体(协程的普通列表)的情况?

4个回答

164

实际信息:

从Python 3.7开始,为此目的添加了高级函数asyncio.create_task(coro)

您应该使用它来代替从协程创建任务的其他方式。但是,如果您需要从任意可等待对象创建任务,则应使用asyncio.ensure_future(obj)


旧信息:

ensure_futurecreate_task

ensure_future是一种从协程创建任务的方法。它根据参数以不同的方式创建任务(包括使用create_task来处理协程和类似于future的对象)。

create_taskAbstractEventLoop的一个抽象方法。不同的事件循环可以以不同的方式实现此函数。

您应该使用ensure_future来创建任务。只有当您要实现自己的事件循环类型时,才需要使用create_task

更新:

@bj0指出了Guido在这个主题上的回答

ensure_future() 的作用是,如果你有一个既可以是协程也可以是 Future(后者包括 Task,因为它是 Future 的子类)的东西,并且你想要调用一个仅在 Future 上定义的方法(可能是唯一有用的例子是 cancel())。当它已经是一个 Future(或 Task)时,这个函数不会执行任何操作;当它是一个协程时,它会将其 封装 在一个 Task 中。

如果你知道自己有一个协程并希望对其进行调度,则应使用正确的 API create_task()。只有在提供一个接受协程或 Future 的 API(像大多数 asyncio 自己的 API 一样)并且需要对其进行一些需要 Future 的操作时才应该调用 ensure_future()

稍后:

最后,我仍然认为ensure_future()是一个适当模糊的名称,用于很少需要的功能。在从协程创建任务时,应该使用名为loop.create_task()的函数。也许应该为其提供一个别名asyncio.create_task()?

这令我感到惊讶。一直以来我使用ensure_future的主要动机是它比循环的成员create_task更高级(讨论contains中有添加asyncio.spawnasyncio.create_task等想法)。我还可以指出,在我看来,使用可以处理任何Awaitable而不仅仅是协程的通用函数非常方便。

然而,Guido的回答很清晰:"在从协程创建任务时,应该使用名为loop.create_task()的函数"

什么时候应该将协程封装在任务中?

将协程包装在任务中是一种在后台启动该协程的方法。以下是示例:
import asyncio


async def msg(text):
    await asyncio.sleep(0.1)
    print(text)


async def long_operation():
    print('long_operation started')
    await asyncio.sleep(3)
    print('long_operation finished')


async def main():
    await msg('first')

    # Now you want to start long_operation, but you don't want to wait it finised:
    # long_operation should be started, but second msg should be printed immediately.
    # Create task to do so:
    task = asyncio.ensure_future(long_operation())

    await msg('second')

    # Now, when you want, you can await task finised:
    await task


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

输出:

first
long_operation started
second
long_operation finished

你可以将asyncio.ensure_future(long_operation())替换为await long_operation(),以感受差异。

4
根据Guido的说法,如果你真的需要任务对象,那么你应该使用create_task,但通常情况下你不需要它:https://github.com/python/asyncio/issues/477#issuecomment-268709555 - bj0
ensure_future 会自动将创建的 Task 添加到主事件循环中吗? - AlQuemist
@AlQuemist 每个协程、future 或任务都会自动绑定到某个事件循环中,在稍后执行。默认情况下,它是当前线程的 current 事件循环,但你可以使用 loop 关键字参数指定其他事件循环(请参阅 ensure_future 签名)。 - Mikhail Gerasimov
3
@laycat,我们需要在msg()函数内使用await关键字,以便在第二次调用时将控制权返回给事件循环。一旦事件循环接收到控制权,就能够开始执行long_operation()函数。这样可以演示如何使用ensure_future并发地启动协程以与当前的执行流同时运行。 - Mikhail Gerasimov
2
@garej 如果你将其删除,你就不应该看到最后的输出 long_operation finished,因为 main()(以及整个事件循环)在 long_operation() 任务之前就已经结束了。我猜在 Jupyter 中运行脚本可能不是这种情况,但无论如何,await task 的想法是我们需要等待任务完成。 - Mikhail Gerasimov
显示剩余7条评论

55

create_task()

  • 接受协程(coroutines),
  • 返回任务(Task),
  • 在事件循环(loop)的上下文(context)中被调用。

ensure_future()

  • 接受未来对象(Futures)、协程、可等待对象(awaitable objects),
  • 返回任务(Task)(如果传递了 Future,则返回 Future)。
  • 如果给定的参数是协程,则使用create_task
  • 可以传递事件循环对象。

正如您所看到的create_task更加具体。


async函数没有使用create_taskensure_future

简单地调用async函数会返回协程。

>>> async def doit(i):
...     await asyncio.sleep(3)
...     return i
>>> doit(4)   
<coroutine object doit at 0x7f91e8e80ba0>

由于底层的 gather 函数确保了参数是 Futures,因此明确使用 ensure_future 是多余的。

类似问题请参考 「loop.create_task」、「asyncio.async/ensure_future」和「Task」有什么区别?


不确定这是否是一个好问题,但确保未来似乎只是创建一个包装器,以便稍后可以等待/调用您的协程?不确定为什么需要这两者,我们不能直接等待我们的协程或类似的东西。 - Charlie Parker

22
注意:仅适用于 Python 3.7(有关 Python 3.5,请参阅早期答案)。
来自官方文档: asyncio.create_task(在Python 3.7中添加)是生成新任务的首选方式,而不是ensure_future()

细节:

因此,现在在Python 3.7及以上版本中,有两个顶层包装函数(类似但不同):

好的,最终这两个包装函数都将帮助您调用BaseEventLoop.create_task。唯一的区别是ensure_future接受任何awaitable对象,并帮助您将其转换为Future。此外,您可以在ensure_future中提供自己的event_loop参数。根据您是否需要这些功能,您可以简单地选择要使用的包装器。


1
我认为还有一个未记录的区别:如果在运行循环之前尝试调用asyncio.create_task,您将会遇到问题,因为asyncio.create_task期望一个正在运行的循环。在这种情况下,您可以使用asyncio.ensure_future,因为它不需要运行中的循环。 - coelhudo
不确定这是否是一个好问题,但确保未来似乎只是创建一个包装器,以便稍后可以等待/调用您的协程?不确定为什么需要这两者,我们不能直接等待我们的协程或类似的东西。 - Charlie Parker

3

举例来说,这三种类型都是异步执行的。唯一的区别在于在第三个例子中,您预先生成了所有10个协程,并一起提交到循环中。因此,只有最后一个会随机输出。


不确定这是否是一个好问题,但确保未来似乎只是创建一个包装器,以便稍后可以等待/调用您的协程?不确定为什么需要这两者,我们不能直接等待我们的协程或类似的东西。 - Charlie Parker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接