代码之家  ›  专栏  ›  技术社区  ›  Daniel Mahler

Python本机协程和send()

  •  40
  • Daniel Mahler  · 技术社区  · 10 年前

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

    当新的本地人 async/await 协同程序为异步I/O提供了卓越的支持,我不知道如何获得 发送() 与他们一起。使用 yield 在里面 async 函数被明确禁止,因此本机协程只能使用 return 陈述虽然 await 表达式将新值带入协程,这些值来自被调用方,而不是调用方,并且每次等待的调用都是从开始计算的,而不是从结束的地方计算的。

    有没有一种方法可以恢复返回的协程,并可能发送一个新值? 我怎样才能模仿大卫·比兹利的技巧 Curious Course on Coroutines and Concurrency 使用本地协同程序?

    我想到的一般代码模式是

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

    在呼叫者中

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

    编辑

    我接受了凯文的回答,但我注意到政治公众人物 says

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

    ...

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

    所以显然,本地的协同程序确实有一个 发送() ? 如果没有 产量 表达式来接收协程中的值?

    2 回复  |  直到 10 年前
        1
  •  29
  •   plamut Hadi Akbarzadeh    6 年前

    在学习了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 ,因为我们已经有了自己的事件循环来驱动整个过程 计划程序

    可等待的对象

    本机协同程序无法立即工作,以下代码会导致错误:

    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__() products被冒泡到发送值的最顶层代码。

    整个过程对 f_科罗 协程,它不直接参与,也不看到值被上下传递。然而,当 等待 的迭代器已用尽 回来 值成为 等候 表达式(本例中为42) f_科罗 最终恢复。

    请注意 等候 协程中的表达式也可以链接。一个协程可以等待另一个等待另一协程的协程…直到整个链以 yield 在路上的某个地方。

    将值发送到协程本身

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

    为了让验尸官提出 系统调用 直到调度程序 系统调用 实例可以简单地 收益率本身 ,并且它将如前一节所述被引导到调度器。

    因此,第一个需要的更改是将此逻辑添加到基础 系统调用 类别:

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

    使用 系统调用 实例可等待,下面的程序现在实际运行:

    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__() 方法如果我们想将值带入协程,系统调用必须 回来 使其成为 等候 协程中的表达式。

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

    使用修改的运行相同的代码 系统调用 产生期望的输出:

    I'm foo 1
    Task 1 terminated
    

    同时运行协同程序

    我们仍然需要一种暂停协同程序的方法,即使用系统“陷阱”代码。在课程材料中,这是用素材完成的 产量 在协同程序中,但尝试使用普通 等候 实际上是一个语法错误:

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

    幸运的是,解决方法很简单。由于我们已经有了工作的系统调用,我们可以添加一个虚拟的no-op系统调用,它的唯一任务是挂起协同程序并立即重新调度它:

    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
    

    脚注

    这个 计划程序 代码完全不变。

    它只是作品

    这显示了原始设计的美妙之处,其中调度器和在其中运行的任务没有相互耦合,我们能够在没有 计划程序 知道这一点。即使是 Task 包装协程的类不必更改。

    不需要蹦床。

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

    不需要这种机制,因为 等候 (和它的老同伴, 收益率 )正如开头所解释的那样,已经使这种链接成为可能。

    附录A-一个完全可运行的示例(需要Python 3.5+)

    示例_完整.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()
    
        2
  •  12
  •   Kevin    10 年前

    有没有一种方法可以恢复返回的协程,并可能发送一个新值?

    async await 只是 句法糖 yield from 。当协程返回时(使用 return 声明),仅此而已。框架不见了。这是不可恢复的。这正是发电机一直以来的工作方式。例如:

    def foo():
        return (yield)
    

    你能做到的 f = foo(); next(f); f.send(5) ,你会回来的。但是如果你尝试 f.send() 同样,它不起作用,因为您已经从帧中返回。 f 不再是带电发电机。

    现在,对于新的协同程序,据我所知,它似乎是屈服的,发送是为事件循环和某些基本谓词之间的通信而保留的,例如 asyncio.sleep() 。协程产生 asyncio.Future 对象发送到事件循环,一旦相关操作完成(它们通常通过 call_soon() 以及其他事件循环方法)。

    您可以通过等待来生成未来的对象,但它不是像这样的通用接口 .send() 是它专门用于事件循环实现。如果您没有实现事件循环,您可能不想玩这个。如果你 实现一个事件循环,你需要问自己为什么 asyncio 不足以满足您的目的并解释什么 明确地 在我们能帮助你之前,你正在努力。

    请注意 收益率 不推荐使用。如果您想要完全不绑定到事件循环的协同程序,只需使用它。 异步 等候 specifically designed for asynchronous programming with event loops 如果这不是你在做的,那么 异步 等候 是错误的工具。

    还有一件事:

    使用 yield 在异步函数中是明确禁止的,因此本机协程只能使用 回来 陈述

    等候 表达 产量控制。 await something() 完全类似于 yield from something() 。他们只是更改了名称,以便对不熟悉发电机的人更直观。


    对于那些真正有兴趣实现自己的事件循环的人来说, here's some example code 示出了(非常小的)实现。这个事件循环非常精简,因为它被设计为同步运行某些特别编写的协同程序,就像它们是正常函数一样。它不能提供您期望的真实 BaseEventLoop 实现,并且不能安全地与任意协同程序一起使用。

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