用 greenlet 协程处理异步事件
自从 PyCon 2011 协程成为热点话题以来,我一直对此有着浓厚的兴趣。为了异步,我们曾使用多线程编程。然而线程在有着 GIL 的 Python 中带来的性能瓶颈和多线程编程的高出错风险,“协程 + 多进程”的组合渐渐被认为是未来发展的方向。技术容易更新,思维转变却需要一个过渡。我之前在异步事件处理方面已经习惯了回调 + 多线程的思维方式,转换到协程还非常的不适应。这几天我非常艰难地查阅了一些资料并思考,得出了一个可能并不可靠的总结。尽管这个总结的可靠性很值得怀疑,但是我还是决定记录下来,因为我觉得既然是学习者,就不应该怕无知。如果读者发现我的看法有偏差并指出来,我将非常感激。
多线程下异步编程的方式
|
1
|
// 在打开了豆瓣首页的标签页// 打开了一个 firebut/chrome console 测试var http = new XMLHttpRequest();// 第三个参数为 false 代表不使用异步http.open("GET", "/site", false);// 发送请求http.send();// 填充响应,一秒钟变页面document.write(http.response);
|
|
1
2
3
4
5
6
7
8
|
var http = new XMLHttpRequest();http.open("GET", "/site", true);// 现在必须使用回调函数http.onreadystatechange = function() {
if (http.readyState == http.DONE) {
if (http.status == 200) {
document.write(http.response);
}
} else if (http.readyState == http.LOADING) {
document.write("正在加载<br />");
}};http.send();
|
|
1
2
|
$.get("/site", function(response){
document.write(http.response);});
|
|
1
2
|
// 别在 IE 下试,IE 的函数名不一样。window.addEventListener("load", function(){
// do something}, false);
|
用多线程实现异步的弊病
Python 中的协程其实 Python 语言内置了协程的支持,也就是我们一般用来制作迭代期的“生成器”(Generator)。生成器本身不是一个完整的协程实现,所以此外 Python 的第三方库中还有一个优秀的替代品 greenlet [3] 。
使用生成器作为协程支持,可以实现简单的事件调度模型:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
from time import sleep# Event Managerevent_listeners = {}def fire_event(name):[/color]
[color=#000000] event_listeners[name]()def use_event(func):[/color]
[color=#000000] def call(*args, **kwargs):[/color]
[color=#000000] generator = func(*args, **kwargs)[/color]
[color=#000000] # 执行到挂起[/color]
[color=#000000] event_name = next(generator)[/color]
[color=#000000] # 将“唤醒挂起的协程”注册到事件管理器中[/color]
[color=#000000] def resume():[/color]
[color=#000000] try:[/color]
[color=#000000] next(generator)[/color]
[color=#000000] except StopIteration:[/color]
[color=#000000] pass[/color]
[color=#000000] event_listeners[event_name] = resume[/color]
[color=#000000] return call# Test@use_eventdef test_work():[/color]
[color=#000000] print("=" * 50)[/color]
[color=#000000] print("waiting click")[/color]
[color=#000000] yield "click" # 挂起当前协程, 等待事件[/color]
[color=#000000] print("clicked !!")if __name__ == "__main__":[/color]
[color=#000000] test_work()[/color]
[color=#000000] sleep(3) # 做了很多其他事情[/color]
[color=#000000] fire_event("click") # 触发了 click 事件
|
测试运行可以看到,打印出“waiting click”之后,暂停了三秒,也就是协程被挂起,控制权回到主控制流上,之后触发“click”事件,协程被唤醒。协程的这种“挂起”和“唤醒”机制实质上是将一个过程切分成了若干个子过程,给了我们一种以扁平的方式来使用事件回调模型。
用 greenlet 实现简单事件框架
用生成器实现的协程有些繁琐,同时生成器本身也不是完整的协程实现,因此经常有人批评 Python 的协程比 Lua 弱。其实 Python 中只要放下生成器,使用第三方库 greenlet,就可以媲美 Lua 的原生协程了。greenlet 提供了在协程中直接切换控制权的方式,比生成器更加灵活、简洁。
基于把协程看成“切开了的回调”的视角,我使用 greenlet 制作了一个简单的事件框架。
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
from greenlet import greenlet, getcurrentclass Event(object):[/color]
[color=#000000] def __init__(self, name):[/color]
[color=#000000] self.name = name[/color]
[color=#000000] self.listeners = set()[/color]
[color=#000000] def listen(self, listener):[/color]
[color=#000000] self.listeners.add(listener)[/color]
[color=#000000] def fire(self):[/color]
[color=#000000] for listener in self.listeners:[/color]
[color=#000000] listener()class EventManager(object):[/color]
[color=#000000] def __init__(self):[/color]
[color=#000000] self.events = {}[/color]
[color=#000000] def register(self, name):[/color]
[color=#000000] self.events[name] = Event(name)[/color]
[color=#000000] def fire(self, name):[/color]
[color=#000000] self.events[name].fire()[/color]
[color=#000000] def await(self, event_name):[/color]
[color=#000000] self.events[event_name].listen(getcurrent().switch)[/color]
[color=#000000] getcurrent().parent.switch()[/color]
[color=#000000] def use(self, func):[/color]
[color=#000000] return greenlet(func).switch
|
使用这个事件框架,可以很容易的完成挂起过程 -> 转移控制权 -> 事件触发 -> 唤醒过程的步骤。还是上文生成器协程中使用的例子,用基于 greenlet 的事件框架实现出来是这样的:
|
01
02
03
04
05
06
07
08
09
10
|
from time import sleepfrom event import EventManagerevent = EventManager()event.register("click")@event.usedef test(name):[/color]
[color=#000000] print "=" * 50[/color]
[color=#000000] print "%s waiting click" % name[/color]
[color=#000000] event.await("click")[/color]
[color=#000000] print "clicked !!"if __name__ == "__main__":[/color]
[color=#000000] test("micro-thread")[/color]
[color=#000000] print "do many other works..."[/color]
[color=#000000] sleep(3) # do many other works[/color]
[color=#000000] print "done... now trigger click event."[/color]
[color=#000000] manager.fire("click")
|
|
1
2
3
4
|
micro-thread waiting click
do many other works...
done... now trigger click event.
clicked !!
|
总结 总的来说,我个人感觉协程给了我们一种更加轻量的异步编程方式。在这种方式中没有调度复杂的系统级线程,没有容易出错的临界资源,反而走了一条更加透明的路 —— 显式的切换控制权代替调度器充满“猜测”的调度算法,放弃进程内并发使用清晰明了的串行方式。结合多进程,我想协程在异步编程尤其是 Python 异步编程中的应用将会越来越广
源于:https://my.oschina.net/u/2260265/blog/411907
本文探讨了Python中使用greenlet协程处理异步事件的方法,对比了多线程编程的弊端,介绍了协程的基本原理及其实现,包括生成器和greenlet库的使用。
386

被折叠的 条评论
为什么被折叠?



