在完成了 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()
回溯(最近一次通话最后):
...
mytid = 等待 GetTid()
类型错误:对象 GetTid 不能在“等待”表达式中使用
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()
输出:
我是 foo 没有
任务 1 终止
太好了,它不再崩溃了!
但是协程没有收到任务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 运行相同的代码会产生所需的输出:
我是富1
任务 1 终止
同时运行协程
我们仍然需要一种暂停协程的方法,即拥有一个系统“陷阱”代码。在课程材料中,这是在协程中使用纯 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()
输出:
我是富1
我是酒吧 2
我是富1
我是酒吧 2
我是富1
我是酒吧 2
任务 1 终止
我是酒吧 2
我是酒吧 2
任务 2 终止
脚注
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()