虽然Python 3.7+已经支持了asyncio,可以直接采用async和await关键字编写协程了。但是为了理解协程调度原理,我决定还是写一个简单的生成器协程框架。
#!/usr/env/python3 import time import datetime timeoutList = [] # Should contains (timeout, callback) pairs def event_loop(): while True: while len(timeoutList): timeout, callback = timeoutList.pop(0) if timeout < time.time(): callback() else: timeoutList.append((timeout, callback)) break time.sleep(0.01) # sleep 10ms for less cpu load def set_timeout(timeout, callback): timeoutList.append((time.time() + timeout, callback)) def hello(): d = datetime.datetime.now() print("hello {}".format(d.strftime("%H:%M:%S"))) set_timeout(1, hello) set_timeout(0, hello) event_loop()
以上代码是一个简单的采用异步调用形式的程序,这个程序会不断打印 hello 和当前时间,下面是它的输出:
(base) ➜ tinyevent python async_hello.py hello 12:34:52 hello 12:34:53 hello 12:34:54 hello 12:34:55 hello 12:34:56 hello 12:34:57 hello 12:34:58 hello 12:34:59
在event_loop函数中,循环不断遍历timeoutList,查找已经到期的timeout函数。set_timeout函数将一个回调函数注册到timeoutList中。event_loop模拟了一个通常的事件循环,遍历消息队列,并调用注册了指定消息的回调函数。
下面,让我们把这一程序改写成协程形式:
#!/usr/env/python3 import time import datetime timeoutList = [] # Should contains (timeout, callback) pairs def event_loop(): while True: while len(timeoutList): timeout, callback = timeoutList.pop(0) if timeout < time.time(): callback() else: timeoutList.append((timeout, callback)) break time.sleep(0.01) # sleep 10ms for less cpu load def set_timeout(timeout, callback): timeoutList.append((time.time() + timeout, callback)) def coro_run(gene): coro = gene() def callback(): try: context = next(coro) context(callback) except StopIteration as e: pass set_timeout(0, callback) def coro_sleep(timeout): def context(callback): set_timeout(timeout, callback) return context def coro_hello(): while True: d = datetime.datetime.now() print("hello {}".format(d.strftime("%H:%M:%S"))) yield coro_sleep(1) coro_run(coro_hello) event_loop()
这一程序与之前的程序并无太大不同,只是 hello 函数变成了coro_hello “函数”,严格来讲,coro_hello 并不是一个函数,而是一个生成器。而 coro_run 函数则是整个程序中看起来最 tricky 的地方。
coro_run 函数接收一个生成器对象,并调用之,产生一个可迭代对象。在这个函数中,我们构建了一个闭包,作为事件循环中将要注册的回调函数。在这个回调函数中,对可迭代对象使用next,获取它的输出。在此,我们假设它输出的是一个函数,这个函数会将我们的 callback 注册到事件循环上,从而当被注册的事件发生时,callback 及 协程本身能够被再次调用。
coro_sleep 就是那个会产生注册callback用的函数的“工厂函数”,它返回一个闭包供上层的callback调用,闭包将 callback注册到下一个timeout 时间上。
至此,在生成器中就可以使用 coro_sleep 实现协程的睡眠了。我们可以很清晰的看到,无论是这里的 yield 还是 asyncio 用的 await,本质就是将协程的下一次复苏注册到指定的事件上。