从协程中`yield`和从任务中`yield`的区别

8 浏览
0 Comments

从协程中`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、文档、核心开发者注释)的链接。

0
0 Comments

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关键字来解决这个问题。

0
0 Comments

在使用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.asynctest1包装在一个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.asyncasyncio.ensure_future取代。

0
0 Comments

在调用方协程中,使用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明确说明了这一点。

就是这样。现在我有了启示,谢谢你!我认为您应该使用此信息更新答案。

0