Python原生协同程序和send()

时间:2015-12-26 06:25:15

标签: python async-await coroutine

基于生成器的协同程序具有send()方法,该方法允许调用方和被调用方之间的双向通信,并从调用方恢复生成的生成器协同程序。这是将生成器转换为协同程序的功能。

虽然新的原生async/await协同程序为异步I / O提供了更好的支持,但我没有看到如何使用它们获得等效的send()。明确禁止在yield函数中使用async,因此本机协同程序只能使用return语句返回一次。虽然await表达式将新值带入协程,但这些值来自被叫者,而不是来电者,等待的呼叫每次从头开始计算,而不是从中断的位置开始计算。

有没有办法从它停止的位置恢复返回的协程并可能发送新值? 如何使用本机协同程序模拟David Beazley Curious Course on Coroutines and Concurrency中的技术?

我想到的一般代码模式类似于

def myCoroutine():
  ...
  while True:
    ...
    ping = yield(pong)
    ...

并且在来电者中

while True:
  ...
  buzz = myCoroutineGen.send(bizz)
  ...

修改

我接受了凯文的回答,但我注意到了PEP says

  

协同程序在内部基于生成器,因此它们共享实现。与生成器对象类似,协同程序具有throw(),send()和close()方法。

...

  

用于协同程序的throw(),send()方法用于推送值并将错误引发到类似Future的对象中。

显然,原生协程确实有send()?如果没有yield表达式来接收协程中的值,它如何工作?

2 个答案:

答案 0 :(得分:9)

在完成了Beazley关于协同程序的相同(很神奇,我必须说)课程之后,我问了自己一个同样的问题-如何调整代码以与Python 3.5中引入的本地协同程序一起工作?

事实证明,只需对代码进行较小的更改即可完成 。我将假定读者熟悉本课程的材料,并以pyos4.py版本为基础-支持“系统调用”的第一个Scheduler版本。

提示:最后,可以在附录A 中找到完整的可运行示例。

客观

目标是转换以下协程代码:

def foo():
    mytid = yield GetTid()  # a "system call"
    for i in xrange(3):
        print "I'm foo", mytid
        yield  # a "trap"

...变成本地协程,并且仍然像以前一样使用:

async def foo():
    mytid = await GetTid()  # a "system call"
    for i in range(3):
        print("I'm foo", mytid)
        await ???  # a "trap" (will explain the missing bit later)

我们希望在没有asyncio的情况下运行它,因为我们已经拥有驱动整个过程的事件循环-这是Scheduler类。

可等待的对象

原生协程不能立即起作用,以下代码会导致错误:

async def foo():
    mytid = await GetTid()
    print("I'm foo", mytid)

sched = Scheduler()
sched.new(foo())
sched.mainloop()
Traceback (most recent call last):
    ...
    mytid = await GetTid()
TypeError: object GetTid can't be used in 'await' expression

PEP 492解释了可以等待什么样的对象。选项之一是“带有__await__方法的对象返回迭代器”

就像yield from一样,如果您熟悉它,await充当等待对象与驱动协程的最外层代码之间的隧道(通常是事件循环)。最好用一个例子来说明:

class Awaitable:
    def __await__(self):
        value = yield 1
        print("Awaitable received:", value)
        value = yield 2
        print("Awaitable received:", value)
        value = yield 3
        print("Awaitable received:", value)
        return 42


async def foo():
    print("foo start")
    result = await Awaitable()
    print("foo received result:", result)
    print("foo end")

以交互方式驱动foo()协程会产生以下结果:

>>> f_coro = foo()  # calling foo() returns a coroutine object
>>> f_coro
<coroutine object foo at 0x7fa7f74046d0>
>>> f_coro.send(None)
foo start
1
>>> f_coro.send("one")
Awaitable received: one
2
>>> f_coro.send("two")
Awaitable received: two
3
>>> f_coro.send("three")
Awaitable received: three
foo received result: 42
foo end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

无论发送到f_coro的内容如何,​​都将被引导到Awaitable实例中。同样,Awaitable.__await__()产生的结果都会冒泡,直到发送值的最顶层代码。

整个过程对f_coro协程是透明的,协程没有直接参与,也看不到值上下传递。但是,当Awaitable的迭代器用尽时,其 return 值将成为await表达式的结果(在本例中为42),这就是f_coro终于恢复了。

请注意,协程中的await表达式也可以链接。一个协程可以等待另一个协程,然后等待另一个协程...直到整个链以yield结尾在路上。

将值发送到协程本身

这些知识如何帮助我们?好吧,在课程材料中,协程可以产生SystemCall实例。调度程序可以理解这些内容,并让系统调用处理请求的操作。

为了使协程程序将SystemCall带到调度程序,SystemCall实例可以简单地 yield本身,并将其作为调度程序引导到调度程序。在上一节中进行了介绍。

因此,第一个必需的更改是将此逻辑添加到基类SystemCall

class SystemCall:
    ...
    def __await__(self):
        yield self

在等待SystemCall实例的情况下,实际上可以运行以下内容:

async def foo():
    mytid = await GetTid()
    print("I'm foo", mytid)

>>> sched = Scheduler()
>>> sched.new(foo())
>>> sched.mainloop()

输出:

I'm foo None
Task 1 terminated

太好了,它不再崩溃了!

但是,协程没有收到任务ID,而是获得了None。这是因为由系统调用的handle()方法设置并由Task.run()方法发送的值:

# in Task.run()
self.target.send(self.sendval)

...以SystemCall.__await__()方法结束。如果我们想将值带到协程中,则系统调用必须返回,以便它成为协程中await表达式的值。

class SystemCall:
    ...
    def __await__(self):
        return (yield self)

使用修改后的SystemCall运行相同的代码会产生所需的输出:

I'm foo 1
Task 1 terminated

同时运行协程

我们仍然需要一种暂停协程的方法,即具有系统“陷阱”代码。在课程材料中,这是在协程内部使用普通yield完成的,但是尝试使用普通await实际上是语法错误:

async def foo():
    mytid = await GetTid()
    for i in range(3):
        print("I'm foo", mytid)
        await  # SyntaxError here

幸运的是,解决方法很容易。由于我们已经有正常的系统调用,因此我们可以添加一个虚拟的无操作系统调用,其唯一的工作就是挂起协程并立即对其进行重新调度:

class YieldControl(SystemCall):
    def handle(self):
        self.task.sendval = None   # setting sendval is optional
        self.sched.schedule(self.task)

在任务上设置sendval是可选的,因为预计该系统调用不会产生任何有意义的值,但是我们选择使其明确。

我们现在已具备运行多任务操作系统的所有条件!

async def foo():
    mytid = await GetTid()
    for i in range(3):
        print("I'm foo", mytid)
        await YieldControl()


async def bar():
    mytid = await GetTid()
    for i in range(5):
        print("I'm bar", mytid)
        await YieldControl()


sched = Scheduler()
sched.new(foo())
sched.new(bar())
sched.mainloop()

输出:

I'm foo 1
I'm bar 2
I'm foo 1
I'm bar 2
I'm foo 1
I'm bar 2
Task 1 terminated
I'm bar 2
I'm bar 2
Task 2 terminated

脚语

Scheduler代码完全不变。

它。只是。有效。

这显示了原始设计的美妙之处,其中调度程序和其中运行的任务没有相互耦合,并且我们能够在Scheduler不知道的情况下更改协程实现。甚至包装协程的Task类也不必更改。

不需要油光油。

在系统的pyos8.py版本中,实现了蹦床的概念。它允许协程在程序程序的帮助下将一部分工作委托给另一个协程(调度程序代表父协程调用子协程,并将前者的结果发送给父协程)。

不需要这种机制,因为await(及其较旧的伴侣,yield from)已经使这种链接成为可能,如开头所述。

附录A-完整的示例(需要Python 3.5 +)

example_full.py
from queue import Queue


# ------------------------------------------------------------
#                       === Tasks ===
# ------------------------------------------------------------
class Task:
    taskid = 0
    def __init__(self,target):
        Task.taskid += 1
        self.tid = Task.taskid   # Task ID
        self.target = target        # Target coroutine
        self.sendval = None          # Value to send

    # Run a task until it hits the next yield statement
    def run(self):
        return self.target.send(self.sendval)


# ------------------------------------------------------------
#                      === Scheduler ===
# ------------------------------------------------------------
class Scheduler:
    def __init__(self):
        self.ready = Queue()   
        self.taskmap = {}        

    def new(self,target):
        newtask = Task(target)
        self.taskmap[newtask.tid] = newtask
        self.schedule(newtask)
        return newtask.tid

    def exit(self,task):
        print("Task %d terminated" % task.tid)
        del self.taskmap[task.tid]

    def schedule(self,task):
        self.ready.put(task)

    def mainloop(self):
         while self.taskmap:
            task = self.ready.get()
            try:
                result = task.run()
                if isinstance(result,SystemCall):
                    result.task  = task
                    result.sched = self
                    result.handle()
                    continue
            except StopIteration:
                self.exit(task)
                continue
            self.schedule(task)


# ------------------------------------------------------------
#                   === System Calls ===
# ------------------------------------------------------------
class SystemCall:
    def handle(self):
        pass

    def __await__(self):
        return (yield self)


# Return a task's ID number
class GetTid(SystemCall):
    def handle(self):
        self.task.sendval = self.task.tid
        self.sched.schedule(self.task)


class YieldControl(SystemCall):
    def handle(self):
        self.task.sendval = None   # setting sendval is optional
        self.sched.schedule(self.task)


# ------------------------------------------------------------
#                      === Example ===
# ------------------------------------------------------------
if __name__ == '__main__':
    async def foo():
        mytid = await GetTid()
        for i in range(3):
            print("I'm foo", mytid)
            await YieldControl()


    async def bar():
        mytid = await GetTid()
        for i in range(5):
            print("I'm bar", mytid)
            await YieldControl()

    sched = Scheduler()
    sched.new(foo())
    sched.new(bar())
    sched.mainloop()

答案 1 :(得分:7)

  

有没有办法从它停止的位置恢复返回的协同程序并可能发送新值?

没有

asyncawait 只是 yield from的语法糖。当一个协程返回时(带有return语句),那就是它。框架消失了。它不可恢复。这正是发电机一直有效的方式。例如:

def foo():
    return (yield)

你可以f = foo(); next(f); f.send(5),然后你会回来5.但是如果你再次尝试f.send(),它就行不通,因为你已经从帧中返回了。 f不再是实时生成器。

现在,至于新的协同程序,据我所知,似乎屈服和发送被保留用于事件循环和某些基本谓词(如asyncio.sleep())之间的通信。协程生成asyncio.Future个对象直到事件循环,并且一旦关联的操作完成,事件循环将这些相同的未来对象发送回协程(它们通常通过call_soon()和其他事件进行调度循环方法)。

您可以通过等待它们来生成未来的对象,但它不是像.send()这样的通用界面。它专门用于事件循环实现。如果你没有实现一个事件循环,你可能不想玩这个。如果你 实现了一个事件循环,你需要问问自己为什么asyncio中完美的实现不足以满足你的目的并解释具体你是什么在我们可以帮助你之前尝试做。

请注意,我们不会弃用yield from。如果你想要根本没有绑定到事件循环的协同程序,只需使用它。 asyncawaitspecifically designed for asynchronous programming with event loops。如果这不是您正在做的事情,那么asyncawait是错误的工具。

还有一件事:

  

明确禁止在异步函数中使用yield,因此本机协同程序只能使用return语句返回一次。

await个词组执行控制权。 await something()完全类似于yield from something()。他们只是更改了名称,以便对不熟悉发电机的人更直观。

对于那些真正有兴趣实现自己的事件循环的人,here's some example code显示(非常小的)实现。这个事件循环被极度剥离,因为它被设计为同步运行某些特殊编写的协程,就像它们是正常的函数一样。它不能提供您对实际BaseEventLoop实现所期望的全方位支持,并且不能安全地与任意协程一起使用。

通常情况下,我会将代码包含在我的答案中,而不是链接到它,但是存在版权问题,而且答案本身并不重要。