协程yield from与任务yield from之间的区别

29

在2014年关于Tulip/Asyncio的演讲中,Guido van Rossum展示了幻灯片:shows the slide:

任务 vs 协程

  • 比较:

    • res = yield from some_coroutine(...)
    • res = yield from Task(some_coroutine(...))
  • 任务可以在不等待的情况下取得进展

    • 只要你等待其他东西
      • 即yield from

但我完全没有抓住重点。

从我的角度来看,这两个构造是相同的:

对于裸协程 - 它被调度,因此任务已经创建,因为调度程序使用任务,然后调用方协程暂停,直到被调用者完成,然后变得自由继续执行。

对于Task - 全部相同 - 新任务被调度,调用方协程等待其完成。

这两种情况下代码执行的区别以及开发人员应该考虑的影响是什么?

p.s.
非常感谢提供权威来源的链接(GvR、PEP、文档、核心开发人员笔记)。

3个回答

31
对于调用方的协同程序来说,yield from coroutine() 感觉像是一个函数调用(即它会在 coroutine() 完成后再次获得控制)。
而另一方面,yield from Task(coroutine()) 更像是创建一个新线程。 Task() 几乎立即返回,并且很可能在 coroutine() 完成之前调用者就重新获得了控制权。 f()th = threading.Thread(target=f, args=()); th.start(); th.join() 之间的区别是显而易见的,对吧?

2
在asyncio中根本没有优先级。对于裸协程,您必须使用yield from coro()来运行协程,在任务构建的情况下,例如async(coro())将与其他协程并行执行。 - Andrew Svetlov
1
是的,你说得对。从技术上讲,yield from coro() 立即执行协程,而 async(coro()) 通过 loop.call_soon() 调用安排执行。 - Andrew Svetlov
1
这只是实现细节。Trollius不使用yield from,也不完全兼容asyncio。 - Andrew Svetlov
1
是的,如果 PyPy 支持 3.4 版本(目前只支持 3.2 版本),它会以与 CPython 3.4 相同的方式处理协程。至少我认为是这样,但不能保证。我是 CPython 核心开发人员,不是 PyPy 的开发人员。 - Andrew Svetlov
1
是的,PEP 380明确指出了这一点。 - Andrew Svetlov
显示剩余17条评论

17
The point of using asyncio.Task(coro()) is to execute coro in the background while waiting for other tasks without explicitly waiting for it. According to Guido's slide, this means that a Task can make progress without waiting for it, as long as you wait for something else. For example, consider the following.
import asyncio

@asyncio.coroutine
def test1():
    print("in test1")


@asyncio.coroutine
def dummy():
    yield from asyncio.sleep(1)
    print("dummy ran")


@asyncio.coroutine
def main():
    test1()
    yield from dummy()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

输出:

dummy ran

正如您所看到的,test1 实际上从未被执行,因为我们没有显式地在其上调用 yield from

现在,如果我们使用 asyncio.asyncTask 实例包装在 test1 周围,则结果是不同的:

import asyncio

@asyncio.coroutine
def test1():
    print("in test1")


@asyncio.coroutine
def dummy():
    yield from asyncio.sleep(1)
    print("dummy ran")


@asyncio.coroutine
def main():
    asyncio.async(test1())
    yield from dummy()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

输出:

in test1
dummy ran

因为使用yield from asyncio.async(coro())会比没有任何效益的yield from coro()慢,所以实际上没有使用yield from asyncio.async(coro())的实际理由;它引入了将coro添加到内部asyncio调度程序的开销,但这并不需要,因为使用yield from可以保证coro将被执行。如果你只想调用协程并等待它完成,那么直接yield from协程即可。 顺便说一下: 我正在使用asyncio.async*而不是直接使用Task因为文档推荐这样做

不要直接创建Task实例:使用async()函数或BaseEventLoop.create_task()方法。

*请注意,从Python 3.4.4起,asyncio.async已被弃用,推荐使用asyncio.ensure_future


3

如PEP 380所述,它介绍了yield from,表达式res = yield from f()的想法来自以下循环:

for res in f():
    yield res

随着这一点的理解,事情变得非常清晰:如果f()some_coroutine(),那么协程将被执行。另一方面,如果f()Task(some_coroutine()),则会执行Task.__init__,而some_coroutine()不会被执行,只有新创建的生成器作为第一个参数传递给Task.__init__

结论:

  • res = yield from some_coroutine() => 协程继续执行并返回下一个值
  • res = yield from Task(some_coroutine()) => 创建一个新任务,它存储一个未执行的some_coroutine()生成器对象。

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