从协程中`yield`和从任务中`yield`的区别
从协程中`yield`和从任务中`yield`的区别
在2014年关于Tulip/Asyncio的演讲中,Guido van Rossum在他的演讲中展示了幻灯片:点击这里查看:\n
\n任务 vs 协程\n
\n
- 比较:\n
\n
- res = yield from some_coroutine(...)
\n
- res = yield from Task(some_coroutine(...))
\n
\n
- 任务可以在不等待的情况下继续进行\n
\n
- 只要你等待其他东西\n
\n
- 例如,使用yield from
\n
\n
\n
\n
\n我完全不明白其中的要点。\n在我看来,这两种结构是相同的:\n对于裸协程 - 它被调度,所以任务总是会被创建,因为调度程序使用任务,然后调用者协程被暂停,直到被调用者完成,然后才能继续执行。\n对于Task
- 同样的情况 - 新任务被调度,调用者协程等待其完成。\n在这两种情况下,代码的执行方式有什么区别,开发者在实践中应该考虑哪些影响?\n附注:
\n非常感谢提供权威来源(GvR、PEPs、文档、核心开发者注释)的链接。
yield from语法的引入是为了简化协程中的迭代器处理。根据PEP 380中的描述,yield from表达式res = yield from f()
的实际执行效果等同于以下循环:
for res in f(): yield res
通过这个等价关系,我们可以很清楚地理解yield from的工作原理。如果f()
是一个协程函数some_coroutine()
,那么这个协程函数会被执行。而如果f()
是一个Task对象Task(some_coroutine())
,那么会执行Task.__init__
方法,而不会执行some_coroutine()
函数本身,因为只有新创建的生成器对象作为参数传递给Task.__init__
。
根据上述描述,我们可以得出以下结论:
res = yield from some_coroutine()
:协程会继续执行,并返回下一个值res = yield from Task(some_coroutine())
:创建一个新的Task对象,并将未执行的some_coroutine()
生成器对象存储在其中。
在实际应用中,yield from语法的使用可以更加简洁地处理协程中的迭代器。通过将复杂的迭代逻辑交给子生成器处理,可以将代码的可读性和可维护性提高到一个新的水平。同时,yield from还可以将子生成器中的异常传递给委派生成器,使得错误处理变得更加方便。
然而,在使用yield from语法时也会遇到一些问题。当我们使用yield from语法处理Task对象时,可能会出现无法正常返回结果的情况。这是因为Task对象在执行yield from语句时,并不会立即执行协程函数,而是将协程函数的生成器对象传递给Task对象。为了解决这个问题,我们可以使用await
关键字来替代yield from,以确保协程函数被正确执行。
yield from语法的引入是为了简化协程中的迭代器处理。通过将复杂的迭代逻辑交给子生成器处理,可以提高代码的可读性和可维护性。然而,在处理Task对象时可能会出现问题,可以使用await
关键字来解决这个问题。
在使用asyncio.Task(coro())
时,有一种情况是你不想显式地等待coro
执行完成,但是你希望在等待其他任务的同时,coro
在后台被执行。这就是Guido的幻灯片所说的
[A]
Task
可以在等待其他任务的同时取得进展...只要你等待其他任务
考虑下面的例子:
import asyncio .coroutine def test1(): print("in test1") .coroutine def dummy(): yield from asyncio.sleep(1) print("dummy ran") .coroutine def main(): test1() yield from dummy() loop = asyncio.get_event_loop() loop.run_until_complete(main())
输出:
dummy ran
如你所见,test1
实际上没有被执行,因为我们没有在它上面显式调用yield from
。
现在,如果我们使用asyncio.async
将test1
包装在一个Task
实例中,结果就会不同:
import asyncio .coroutine def test1(): print("in test1") .coroutine def dummy(): yield from asyncio.sleep(1) print("dummy ran") .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()
慢而且没有任何好处。它会增加将coro
添加到内部asyncio
调度程序的开销,但是这是不需要的,因为使用yield from
可以保证coro
会被执行。如果你只是想调用一个协程并等待它执行完成,那就直接yield from
这个协程就好了。
附注:
我使用asyncio.async
代替直接使用Task
,因为文档推荐这样做:
不要直接创建
Task
实例:使用async()
函数或BaseEventLoop.create_task()
方法。
*需要注意的是,从Python 3.4.4开始,asyncio.async
被asyncio.ensure_future
取代。
在调用方协程中,使用yield from coroutine()
感觉像是一个函数调用(即当coroutine()完成时,它将再次获得控制权)。
另一方面,yield from Task(coroutine())
感觉更像是创建一个新的线程。Task()
几乎立即返回,并且很可能在coroutine()
完成之前,调用方就会重新获得控制权。
f()
和th = threading.Thread(target=f, args=()); th.start(); th.join()
之间的区别很明显,对吗?
所以区别在于调度程序如何安排执行?裸协程的“优先级”较高,而任务的“优先级”较低?
在asyncio中根本没有优先级。对于裸协程,您必须使用yield from coro()
来运行协程,而对于类似async(coro())
的任务构造,将与其他任务并行执行。
你的意思是裸协程在没有上下文切换的情况下吗?
是的(或者更准确地说,协程可能会导致上下文切换,并且通常会导致上下文切换),但是您必须显式执行它并通过yield from
等待完成。对于任务,您只需启动任务(asyncio.async(coro())
)并自由等待任务完成通过yield from
或继续执行其他操作 - 任务将继续自己的运行。再说一遍,区别就像函数调用和创建新的操作系统线程之间的区别。
我理解你的意思是yield from some_coro()
与yield from Task(some_coro())
之间没有区别 - 对吗?请注意,GvR在他的幻灯片上确实显示了这一点,并且他说了这样的话,就好像存在一些区别。
从最终结果的角度来看,是没有区别的。但是任务的情况会慢一些。在这两种情况下,代码的执行方式是有区别的。
那么区别是什么?yield from Task(some_coro(...))
将在后台执行,而yield form some_coro()
将如何执行?
显然是在当前执行上下文中。如果协程通过loop.run_until_complete(coro())
执行,上下文就是前景。您还可以从任务中调用yield from
。
您能详细说明这里的“前景”和“背景”是什么吗?在这两种情况下都会发生上下文切换,因此这两种情况都可以与启动新线程进行比较,因为它们都是异步的。因此,听起来“前景”是具有较高优先级的异步执行,而“背景”是具有较低优先级的异步执行。
哦。所有上下文的优先级都是一样的。结束。与用户创建的其他线程一样,主线程在python程序中具有相同的优先级。
我不是在说优先级存在,我是在询问有关这里的“背景”和“前景”的一些详细信息。
这个说法正确吗:如果您执行yield from bare_coro()
- bare_coro
将立即执行,没有机会让其他协程在其之前执行?一些代码:gist.github.com/AndrewPashkin/844d17f2ab4f4c88be42
是的,你是对的。从技术上讲,yield from coro()
立即执行协程,async(coro())
通过loop.call_soon()
调用安排执行。
这是实现细节,还是规范中的内容?我在asyncio
文档中找不到任何关于此的内容。也许这是隐含的规范,因此yield from bare_coro()
实际上只是同步的Python代码,当调度程序在调用方协程上调用next()
时,bare_coroutine()
作为常规代码同步调用,没有asyncio
的魔法?
顺便说一句,非常有趣的是,Python 2.7的后端 - trollius
与相同的代码完全不同。
好吧,这是实现细节。Trollius不使用yield from
,并且与asyncio不完全兼容。
与asyncio完全兼容的自定义事件循环使用yield from
并重用调用loop.call_soon()
的asyncio.Task。不完全兼容的系统可能会邀请自己的合同并具有自己的实现细节。
如果考虑到其他Python实现,如PyPy呢?如果他们支持3.4并且严格遵循官方规范,他们是否应该重现这种“同步执行”特性?
是的,如果PyPy支持3.4(目前仅支持3.2),它将以与CPython 3.4相同的方式处理协程。至少我是这样认为,但不能保证。我是CPython核心开发人员,不是PyPy的开发人员。
我的意思是Python语言规范是否断言yield from bare_coro()
在任何实现中都必须像那样工作?
是的,PEP 380明确说明了这一点。
就是这样。现在我有了启示,谢谢你!我认为您应该使用此信息更新答案。