请稍侯

Python协程

28 August 2015

###使用生成器作为协程支持,可以实现简单的事件调度模型:

from time import sleep

# Event Manager

event_listeners = {}

def fire_event(name):
    event_listeners[name]()

def use_event(func):
    def call(*args, **kwargs):
        generator = func(*args, **kwargs)
        # 执行到挂起
        event_name = next(generator)
        # 将“唤醒挂起的协程”注册到事件管理器中
        def resume():
            try:
                next(generator)
            except StopIteration:
                pass
        event_listeners[event_name] = resume
    return call

# Test

@use_event
def test_work():
    print("=" * 50)
    print("waiting click")
    yield "click"  # 挂起当前协程, 等待事件
    print("clicked !!")

if __name__ == "__main__":
    test_work()
    sleep(3)  # 做了很多其他事情
    fire_event("click")  # 触发了 click 事件

测试运行可以看到,打印出waiting click之后,暂停了三秒,也就是协程被挂起,控制权回到主控制流上,之后触发“click”事件,协程被唤醒。 协程的这种“挂起”和“唤醒”机制实质上是将一个过程切分成了若干个子过程,给了我们一种以扁平的方式来使用事件回调模型。

###用greenlet 实现简单事件框架

用生成器实现的协程有些繁琐,同时生成器本身也不是完整的协程实现,因此经常有人批评 Python 的协程比 Lua 弱。其实 Python 中只要放下生成器,使用第三方库 greenlet,就可以媲美 Lua 的原生协程了。greenlet 提供了在协程中直接切换控制权的方式,比生成器更加灵活、简洁。

基于把协程看成“切开了的回调”的视角,我使用 greenlet 制作了一个简单的事件框架。

from greenlet import greenlet, getcurrent


class Event(object):
    def __init__(self, name):
        self.name = name
        self.listeners = set()

    def listen(self, listener):
        self.listeners.add(listener)

    def fire(self):
        for listener in self.listeners:
            listener()


class EventManager(object):
    def __init__(self):
        self.events = {}

    def register(self, name):
        self.events[name] = Event(name)

    def fire(self, name):
        self.events[name].fire()

    def await(self, event_name):
        self.events[event_name].listen(getcurrent().switch)
        getcurrent().parent.switch()

    def use(self, func):
        return greenlet(func).switch

使用这个事件框架,可以很容易的完成挂起过程 -> 转移控制权 -> 事件触发 -> 唤醒过程的步骤。还是上文生成器协程中使用的例子,用基于 greenlet 的事件框架实现出来是这样的:

from time import sleep
from event import EventManager

event = EventManager()
event.register("click")

@event.use
def test(name):
    print "=" * 50
    print "%s waiting click" % name
    event.await("click")
    print "clicked !!"

if __name__ == "__main__":
    test("micro-thread")
    print "do many other works..."
    sleep(3)  # do many other works
    print "done... now trigger click event."
    event.fire("click")