基于生成器的协同程序具有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
表达式来接收协程中的值,它如何工作?
答案 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
实例可以简单地
因此,第一个必需的更改是将此逻辑添加到基类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
)已经使这种链接成为可能,如开头所述。
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)
有没有办法从它停止的位置恢复返回的协同程序并可能发送新值?
没有
async
和await
只是 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
。如果你想要根本没有绑定到事件循环的协同程序,只需使用它。 async
和await
是specifically designed for asynchronous programming with event loops。如果这不是您正在做的事情,那么async
和await
是错误的工具。
还有一件事:
明确禁止在异步函数中使用
yield
,因此本机协同程序只能使用return
语句返回一次。
await
个词组执行控制权。 await something()
完全类似于yield from something()
。他们只是更改了名称,以便对不熟悉发电机的人更直观。
对于那些真正有兴趣实现自己的事件循环的人,here's some example code显示(非常小的)实现。这个事件循环被极度剥离,因为它被设计为同步运行某些特殊编写的协程,就像它们是正常的函数一样。它不能提供您对实际BaseEventLoop实现所期望的全方位支持,并且不能安全地与任意协程一起使用。
通常情况下,我会将代码包含在我的答案中,而不是链接到它,但是存在版权问题,而且答案本身并不重要。