用 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
来源:CSDN
作者:xiaoyaGrace
链接:https://blog.csdn.net/xiaoyaGrace/article/details/103760841