用Python生成器实现协程


虽然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,本质就是将协程的下一次复苏注册到指定的事件上。


发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注