[gevent源码分析] gevent两架马车-libev和greenlet

本篇将讨论gevent的两架马车-libev和greenlet如何协同工作的。

gevent事件驱动底层使用了libev,我们先看看如何单独使用gevent中的事件循环。

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #coding=utf8  
  2. import socket  
  3. import gevent  
  4. from gevent.core import loop  
  5.   
  6. def f():  
  7.     s, address = sock.accept()  
  8.     print address  
  9.     s.send("hello world\r\n")  
  10.   
  11. loop = loop()  
  12. sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  
  13. sock.bind(("localhost",8000))  
  14. sock.listen(10)  
  15. io = loop.io(sock.fileno(),1#1代表read  
  16. io.start(f)  
  17. loop.run()  
代码很简单,使用core.loop新建了一个loop实例,通过io加入socket读事件,通过start设置回调,然后run启动事件循环,一个简单的helloworld服务器搭建好了,可以通过telnet localhost 8000看响应结果。

gevent的整个事件循环是在hub.run中启动的,

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def run(self):  
  2.     assert self is getcurrent(), 'Do not call Hub.run() directly'  
  3.     while True:  
  4.         loop = self.loop  
  5.         loop.error_handler = self  
  6.         try:  
  7.             loop.run()  
  8.         finally:  
  9.             loop.error_handler = None  # break the refcount cycle  
  10.         self.parent.throw(LoopExit('This operation would block forever'))  
上面的self.loop和我们上面自己新建的loop对象是一样的,下面我们通过socket的recv函数看时间是如何注册到loop中。
gevent的socket对象被gevent重新封装,原始socket就是下面的self._sock
我们来看看gevent的socket一次recv做了什么操作。

gevent/socket.py

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def recv(self, *args):  
  2.      sock = self._sock  # keeping the reference so that fd is not closed during waiting  
  3.      while True:  
  4.          try:  
  5.              return sock.recv(*args) # 1.如果此时socket已经有数据,则直接return  
  6.          except error:  
  7.              #没有数据将会抛出异常,且errno为EWOULDBLOCK  
  8.              ex = sys.exc_info()[1]  
  9.              if ex.args[0] != EWOULDBLOCK or self.timeout == 0.0:  
  10.                  raise  
  11.              # QQQ without clearing exc_info test__refcount.test_clean_exit fails  
  12.              sys.exc_clear()  
  13.          #此时将该文件描述符的”读事件“加入到loop中  
  14.          self._wait(self._read_event)  
  15.          """self._wait会调用hub.wait, 
  16.              def wait(self, watcher): 
  17.                  waiter = Waiter() 
  18.                  unique = object() 
  19.                  watcher.start(waiter.switch, unique) #这个watcher就是上面说的loop.io()实例,waiter.switch就是回调函数 
  20.                  try: 
  21.                      result = waiter.get() 
  22.                      assert result is unique, 'Invalid switch into %s: %r (expected %r)' % (getcurrent(), result, unique) 
  23.                  finally: 
  24.                      watcher.stop() 
  25.          当loop捕获到”可读事件“时,将会回调waiter.switch方法,此时将回到这里(因为while循环)继续执行sock.recv(*args) 
  26.          一般来说当重新recv时肯定是可以读到数据的,将直接返回 
  27.          """  
上面的self._read_event = io(fileno, 1),再次回到while大循环中,将直接return sock.recv的结果。我们知道socke.recv(1024)可能返回的并没有1024字节,这要看此时缓冲区已接受多少字节,所以说数据可能一次没有读完,所以可能会触发多次

EWOULDBLOCK,多次读取,只有recv为空字符串时才代表读取结束。典型的读取整个数据一般如下所示:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. buff = []  
  2. while 1:  
  3.     s = socket.recv(1024)  
  4.     if not s:  
  5.         break  
  6.     else:  
  7.         buff.append(s)  
  8. buff = "".jon(buff)  
你可能有点好奇,在gevent中有多处使用了assert判断waiter的返回值,如:hub.wait

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Hub(greenlet):  
  2.     def wait(self, watcher):  
  3.         waiter = Waiter()  
  4.         unique = object()  
  5.         watcher.start(waiter.switch, unique)  
  6.         try:  
  7.             result = waiter.get()  
  8.             assert result is unique, 'Invalid switch into %s: %r (expected %r)' % (getcurrent(), result, unique)  
  9.             #这里为什么要assert?  
  10.             #因为正常肯定是loop调用waiter.switch(unique),那么waiter.get()获取的肯定是unique,  
  11.             #如果不是unique,肯定是有其它地方调用waiter.switch,这很不正常  
  12.         finally:  
  13.             watcher.stop()  

这主要是为了防止回调函数被其它greenlet调用,因为greenlet通过switch传递参数,看下面代码:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def f(t):  
  2.     gevent.sleep(t)  
  3.   
  4. p = gevent.spawn(f,2)  
  5. gevent.sleep(0# 2s后libev将回调f,所以下面p.get获取的是2   
  6. switcher = gevent.spawn(p.switch, 'hello'#强先回调p.switch,传递参数hello  
  7. result = p.get()  
将返回以下异常:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. 将报如下异常:  
  2. AssertionError: Invalid switch into <Greenlet at 0x252c2b0: f(2)>: 'hello' (expected <object object at 0x020414E0>)  
  3. <Greenlet at 0x252c2b0: f(2)> failed with AssertionError  

我们再看看gevent封装的greenlet,

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Greenlet(greenlet):  
  2.     """A light-weight cooperatively-scheduled execution unit."""  
  3.   
  4.     def __init__(self, run=None, *args, **kwargs):  
  5.         hub = get_hub()  
  6.         greenlet.__init__(self, parent=hub)  
  7.         if run is not None:  
  8.             self._run = run  
我们看到所有的Greenlet的parent都是hub,这有什么好处呢?
因为当一个greenlet死掉的时候将回到父greenlet中,也就是hub中,hub将从运行上次回调的地方继续开始事件循环,这也就是为什么事件循环是在hub中运行的理由。

我们来看一个一个Greenlet的生命周期

启动Greenlet需要调用start()方法,

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def start(self):  
  2.     """Schedule the greenlet to run in this loop iteration"""  
  3.     if self._start_event is None:  
  4.         self._start_event = self.parent.loop.run_callback(self.switch)  
也就是将当前的switch加入到loop事件循环中。当loop回调self.switch时将运行run方法(这是底层greenlet提供的),

继承时我们可以提供_run方法。

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def run(self):  
  2.     try:  
  3.         if self._start_event is None:  
  4.             self._start_event = _dummy_event  
  5.         else:  
  6.             self._start_event.stop() #取消之前添加的回调函数,loop将会从回调链中剔除该函数。  
  7.             #libev提供了一系列的对象封装,如io,timer,都有start,stop方法  
  8.             #而回调是通过loop.run_callback开启的,和其它有所不同  
  9.         try:  
  10.             result = self._run(*self.args, **self.kwargs) #运行自定义_run方法  
  11.         except:  
  12.             self._report_error(sys.exc_info())  
  13.             return  
  14.         self._report_result(result) #设置返回结果,这是个比较重要的方法,下面会单独看看  
  15.     finally:  
  16.         pass  
一切顺利,没有异常将调用_report_result方法,我们具体看看:
[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def _report_result(self, result):  
  2.     self._exception = None  
  3.     self.value = result #设置返回结果,可通过get()获取,注意要获取value时  
  4.     #不要直接通过.value,一定要用get方法,因为get()会获取到真正的运行后结果,  
  5.     #而.value那是该Greenlet可能还没结束  
  6.     if self._links and not self._notifier: #这个是干什么的?  
  7.         self._notifier = self.parent.loop.run_callback(self._notify_links)  
为什么说一定要通过get()才能获取最后返回结果呢,因为get()相当于异步的结果返回,那么很有可能Greenlet还没结果我们就调用
get()想获取结果,如果不是异步,肯定是获取不到的。我们看看get()操作,

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def get(self, block=True, timeout=None):  
  2.     """Return the result the greenlet has returned or re-raise the exception it has raised. 
  3.  
  4.     If block is ``False``, raise :class:`gevent.Timeout` if the greenlet is still alive. 
  5.     If block is ``True``, unschedule the current greenlet until the result is available 
  6.     or the timeout expires. In the latter case, :class:`gevent.Timeout` is raised. 
  7.     """  
  8.     if self.ready(): #该Greenlet已经运行结束,直接返回结果  
  9.         if self.successful():  
  10.             return self.value  
  11.         else:  
  12.             raise self._exception  
  13.     if block: #到这里说明该Greenlet并没有结束  
  14.         switch = getcurrent().switch  
  15.         self.rawlink(switch) #将当前Greenlet.switch加到自己的回调链中  
  16.         """ 
  17.         self._links.append(callback) 
  18.         """  
  19.         try:  
  20.             t = Timeout.start_new(timeout)  
  21.             try:  
  22.                 result = self.parent.switch() #切换到hub,可以理解为当前get()阻塞了,当再次回调刚刚注册的switch将回到这里  
  23.                 #可问题是好像我们没有将switch注册到hub中,那是谁去回调的呢?  
  24.                 #幕后黑手其实就是上面的_report_result,当Greenlet结束最后会调用_report_result,  
  25.                 #而_report_result把将_notify_links注册到loop的回调中,最后由_notify_links回调我们刚注册的switch  
  26.                 # def _notify_links(self):  
  27.                 #     while self._links:  
  28.                 #     link = self._links.popleft()  
  29.                 #     try:  
  30.                 #         link(self) #就是这里了,我们看到还把self传给了switch,所以result结果就是self(greenlet通过switch传递结果)  
  31.                 #     except:  
  32.                 #         self.parent.handle_error((link, self), *sys.exc_info())  
  33.                 assert result is self'Invalid switch into Greenlet.get(): %r' % (result, )   
  34.                 #知道为什么result是self的原因了吧  
  35.             finally:  
  36.                 t.cancel()  
  37.         except:  
  38.             self.unlink(switch)  
  39.             raise  
  40.         #运行到这里,其实Greenlet已经结束了,换句话说self.ready()肯定为True  
  41.         if self.ready():  
  42.             if self.successful():  
  43.                 return self.value  
  44.             else:  
  45.                 raise self._exception  
  46.     else#还没结束,你又不等待,没有值返回啊,只能抛出异常了  
  47.         raise Timeout  

通过上面我们知道其实get()就是异步返回结果的方式,当Greenelt要结束时通过run()函数最后的_report_result返回,所以_report_result还是很重要的。

其实_notify_links不只为get提供了最后回调的方法,还提供了Grenlet的link协议。所谓link协议就是Greenlet可以通过

link方法把执行结果传递给一回调函数。

[python]  view plain  copy
  1. def f(source):  
  2.     print source.value  
  3. gevent.spawn(lambda'gg').link(f)  
  4. gevent.sleep(1)  
当Greenlet结束时就会调用f方法,并把self传给f。AsyncResult通过__callback__提供了link方法。
[python]  view plain  copy
  1. from gevent.event import AsyncResult  
  2. a = AsyncResult()  
  3. gevent.spawn(lambda'gg').link(a)  
  4. print a.get()  
  5. gevent.sleep(1)  
看看AsyncEvent的__call__方法,和我们上面的f差不多

[python]  view plain  copy
  1. # link protocol  
  2. def __call__(self, source):  
  3.     if source.successful():  
  4.         self.set(source.value)  
  5.     else:  
  6.         self.set_exception(source.exception)  

其实Greenlet还提供了一个switch_out的方法,在gevent中switch_out是和switch相对应的一个概念,当切换到Greenlet时将

调用switch方法,切换到hub时将调用Greenlet的switch_out方法,也就是给Greenlet一个保存恢复的功能。

gevent中backdoor.py(提供了一个Python解释器的后门)使用了switch,我们来看看

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class SocketConsole(Greenlet):  
  2.   
  3.     def switch(self, *args, **kw):  
  4.         self.saved = sys.stdin, sys.stderr, sys.stdout  
  5.         sys.stdin = sys.stdout = sys.stderr = self.desc  
  6.         Greenlet.switch(self, *args, **kw)  
  7.   
  8.     def switch_out(self):  
  9.         sys.stdin, sys.stderr, sys.stdout = self.saved  

switch_out用的非常漂亮,因为交换环境需要使用sys.stdin,sys.stdout,sys.stderr,所以当切换到我们Greenlet时,

把这三个变量都替换成我们自己的socket描述符,但当要切换到hub时需要恢复这三个变量,所以在switch中先保存,在switch_out中再恢复,switch_out是切换到hub时,与hub的switch调用实现:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Hub(Greenlet):  
  2.     def switch(self):  
  3.         #我们看到的确是先调用先前的Greenlet.switch_out  
  4.         switch_out = getattr(getcurrent(), 'switch_out'None)  
  5.         if switch_out is not None:  
  6.             switch_out()  
  7.         return greenlet.switch(self)  
可以通过下面两句话就启动一个python后门解释器,感兴趣的童鞋可以玩玩。
[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. from gevent.backdoor import BackdoorServer  
  2. BackdoorServer(('127.0.0.1'9000)).serve_forever()  

通过telnet,你可以为所欲为。

在gevent中基本上每个函数都有timeout参数,这主要是通过libev的timer实现。

使用如下:

Timeout对象有pending属性,判断是是否还未运行

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. t=Timeout(1)  
  2. t.start()  
  3. try:  
  4.     print 'aaa'  
  5.     import time  
  6.     assert t.pending == True  
  7.     time.sleep(2)  
  8.     gevent.sleep(0.1)   
  9.     #注意这里不可以是sleep(0),虽然sleep(0)也切换到hub,定时器也到了,但gevent注册的回调  
  10.     #是优先级是高于定时器的(在libev事件循环中先调用callback,然后才是timer)  
  11. except Timeout,e:  
  12.     assert t.pending == False  
  13.     assert e is t #判断是否是我的定时器,和上面的assert一致,防止不是hub调用t.switch  
  14.     print sys.exc_info()  
  15. finally#取消定时器,不管定时器是否可用,都可取消  
  16.     t.cancel()  
Timout对象还提供了with上下文支持:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. with Timeout(1) as t:  
  2.     assert t.pending  
  3.     gevent.sleep(0.5)  
  4. assert not t.pending  
Timeout第二个参数可以自定义异常,如果是Fasle,with上下文将不传递异常
[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. with Timeout(1,False) as t:  
  2.     assert t.pending  
  3.     gevent.sleep(2)  
  4. assert not sys.exc_info()[1]  
  5. 我们看到并没有抛出异常  

还有一个with_timeout快捷方式:

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def f():  
  2.     import time  
  3.     time.sleep(2)  
  4.     gevent.sleep(0.1#不能使用gevent.sleep(0)  
  5.     print 'fff'  
  6.   
  7. t = with_timeout(1,f,timeout_value=10)  
  8. assert t == 10  
注意with_timeout必须有timeout_value参数时才不会抛Timeout异常。

到这里我们对gevnet的底层应该都很熟悉了,对gevent还未介绍到的就是一些高层的东西,如Event,Pool等,后期也会单独拿出来

讲讲。我觉得还需要关注的就是libev的使用,不过这就需要我们深入分析core.pyx的libev cython扩展了,这需要cython的知识,最近我也一直在看源码,后期也会和大家分享。

至于为什么要分析libev的扩展呢?主要是在游戏中有一些定时执行的任务,通过gevent现有的实现比较蹩脚,其实libev提供的timer有两个参数,一个after,一个repeat,after是多久以后启动该定时器,repeat是多次以后再次启动,这刚好满足我的需求,

下面就是我写的一个简单的定时任务脚本,通过gfirefly启动,还提供了web接口。

[python]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #coding:utf-8  
  2. ''''' 
  3. Created on 2014-9-5 
  4.  
  5. @author: http://blog.csdn.net/yueguanghaidao 
  6. '''  
  7. import traceback  
  8. import datetime  
  9. from flask import request  
  10. from gevent.hub import get_hub  
  11. from gtwisted.utils import log  
  12. from gfirefly.server.globalobject import webserviceHandle  
  13. from app.models.role import Role  
  14.   
  15. ''''' 
  16. 定时任务 
  17.  
  18. 任务名 (运行时间(0-24),每次间隔)单位为小时,回调函数均为do_name 
  19. '''  
  20.   
  21. CRONTAB = {  
  22.     "energy": (01), #恢复体力  
  23.     "god_surplustime": (024),  
  24.     "arena_surplustime": (2224),  
  25.     "arena_rewrad": (2124),  
  26.     "sign_reset": (124)  
  27. }  
  28.   
  29.   
  30. def log_except(fun):  
  31.     def wrapper(*args):  
  32.         try:  
  33.             log.msg(fun.__name__)  
  34.             return fun(args)  
  35.         except:  
  36.             log.msg(traceback.format_exc())  
  37.     return wrapper  
  38.   
  39. class Task(object):  
  40.     """所有定时任务 
  41.     """  
  42.     @classmethod  
  43.     @log_except  
  44.     def do_energy(cls):  
  45.         """每一个小时增加1体力(体力小于8) 
  46.         """  
  47.         Role.objects(energy__lt=8).update(inc__energy=1)  
  48.  
  49.     @classmethod  
  50.     @log_except  
  51.     def do_god_surplustime(cls):  
  52.         """财神剩余次数 
  53.         """  
  54.         Role.objects(god__exists=True).update(set__god__surplustime=10)  
  55.  
  56. @webserviceHandle("/cron", methods=['GET''POST'])  
  57. def cron():  
  58.     """提供web接口调用 
  59.     """  
  60.     action = request.args.get("action")  
  61.     if not action:  
  62.         return "action:<br/><br/>"+"<br/>".join(( a for a in CRONTAB))  
  63.     else:  
  64.         try:  
  65.             f = getattr(Task, "do_"+action)  
  66.             try:  
  67.                 f()  
  68.             except:  
  69.                 return traceback.format_exc()  
  70.             return "success"  
  71.         except AttributeError:  
  72.             return "action:<br/><br/>"+"<br/>".join(( a for a in CRONTAB))  
  73.   
  74. def timer(after, repeat):  
  75.     return get_hub().loop.timer(after, repeat)  
  76.   
  77. def run():  
  78.     log.msg("cron start")  
  79.     #配置mongodb  
  80.     mongoconfig.init_Mongo()  
  81.   
  82.     for action, t in CRONTAB.items():  
  83.         log.msg("%s start" % action)  
  84.         f = getattr(Task, "do_"+action)  
  85.         now = datetime.datetime.now()  
  86.         other = now.replace(hour=t[0],minute=0,second=0)  
  87.         if other > now:  
  88.             after = (other-now).seconds  
  89.         else:  
  90.             after = 24*3600-(now-other).seconds  
  91.         #after = t[0]*3600  
  92.         timer(after, t[1]*3600).start(f)  
  93.   
  94. run()  
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值