gevent调度流程解析

来源:互联网 发布:在线教学软件 编辑:程序博客网 时间:2024/06/05 08:02

gevent 是目前应用非常广泛的网络库,高效的轮询IOlibev加上协程(coroutine),使得gevent的性能非常出色,尤其是在web应用中。本文介绍gevent的调度流程,主要包括geventgreenlet的封装和使用,以及greenletlibev的协作。废话不多说,一起来看看吧,希望对大家学习gevent有所帮助。

 

gevent简介:

 

gevent是基于协程( greenlet )的网络库,底层的事件轮训基于 libev (早期是 libevent ),geventAPI概念和Python标准库一致(如事件,队列)gevent有一个很有意思的东西-monkey-patch,能够使python标准库中的阻塞操作变成异步,如socket的读写。

 

gevent来源于 eventlet ,自称比后者实现更简单、API更方便且性能更好,许多开源的web服务器也使用了gevent,如gunicornpaste,当然gevent本生也可以作为一个python web服务器使用。 这篇文章 对常见的wsgi server进行性能对比,gevent不管在http1.0还是http1.1都表现非常出色。下图是目前常用的http1.1标准下的表现:

 

 

gevent高效的秘诀就是greenletlibev啦,greenlet在之前的博文有介绍,geventgreenlet的使用比较限制,只能在两层协程之间切换,简单也不容易出错。libev使用轮训非阻塞的方式进行事件处理,比如unix下的epoll。早期gevent使用libevent,后来替换成libev,因为libev“提供更少的核心功能以求更改的效率”,这里有二者的 性能对比 :

 

 

greenlet回顾:

 

如果想了解gevent的调度流程,最重要的是对greenlet有基本的了解。下面总结一些个人认为比较重要的几点

 

每一个greenlet.greenlet实例都有一个parent(可修改,默认为创生新的greenlet.greenlet所在环境), 当greenlet.greenlet实例执行完逻辑正常结束、或者抛出异常结束时,执行逻辑切回到其parent

可以继承greenlet.greenlet,子类需要实现 run方法 ,当调用greenlet.switch方法时会调用到这个run方法

在gevent中,有两个类继承了greenlet.greenlet,分别是gevent.hub.Hubgevent.greenlet.Greenlet。后文中,如果是greenlet.greenlet这种写法,那么指的是原生的类库greentlet,如果是greenlet(或者Greenlet)那么指gevent封装后的greenlet

 

greenlet调度流程:

 

首先,给出总结性的结论,后面再结合实例和源码一步步分析。

 

每个gevent线程都有一个hub,前面提到hubgreenlet.greenlet的实例。hub实例在需要的时候创生(Lazy Created),那么其parentmain greenlet。之后任何的Greenlet(注意是greenlet.greenlet的子类)实例的parent都设置成hubhub调用libev提供的事件循环来处理Greenlet代表的任务,当Greenlet实例结束(正常或者异常)之后,执行逻辑又切换到hub

 

gevent调度示例1

 

我们看下面最简单的代码:

 

>>> import gevent

 

>>> gevent.sleep(1)

 

>>>

 

上面的代码很简单,但事实上gevent的核心都包含在其中,接下来结合源码进行分析。

 

首先看sleep函数(gevent.hub.sleep):

 

1 def sleep(seconds=0, ref=True):

2     hub = get_hub()

3     loop = hub.loop

4     if seconds <= 0:

5         waiter = Waiter()

6         loop.run_callback(waiter.switch)

7         waiter.get()

8     else:

9         hub.wait(loop.timer(seconds, ref=ref))

首先是获取hub(第2行),然后再hubwait这个定时器事件(第9行)。get_hub源码如下(gevent.hub.get_hub):

 

 1 def get_hub(*args, **kwargs):

 2     """

 3     Return the hub for the current thread.

 4

 5     """

 6     hub = _threadlocal.hub

 7     if hub is None:

 8         hubtype = get_hub_class()

 9         hub = _threadlocal.hub = hubtype(*args, **kwargs)

10     return hub

可以看到,hub是线程内唯一的,之前以提到过greenlet是线程独立的,每个线程有各自的greenlet栈。hubtype默认就是gevent.hub.Hub,在hub的初始化函数(__init__)中,会创建loop属性,默认也就是libevpython封装。

 

回到sleep函数定义,hub.wait(loop.timer(seconds, ref=ref))wait函数源码如下(gevent.hub.Hub.wait):

 

 1     def wait(self, watcher):

 2         """

 3         Wait until the *watcher* (which should not be started) is ready.

 4

 5         """

 6         waiter = Waiter()

 7         unique = object()

 8         watcher.start(waiter.switch, unique)

 9         try:

10             result = waiter.get()

11             if result is not unique:

12                 raise InvalidSwitchError('Invalid switch into %s: %r (expected %r)' % (getcurrent(), result, unique))

13         finally:

14             watcher.stop()

形参watcher就是loop.timer实例,其cython描述在corecext.pyx,我们简单理解成是一个定时器事件就行了。上面的代码中,创建了一个Waitergevent.hub.Waiter)对象,这个对象起什么作用呢,这个类的doc写得非常清楚

 

Waiter.__doc__

 

A low level communication utility for greenlets.

 

Waiter is a wrapper around greenlet's ``switch()`` and ``throw()`` calls that makes them somewhat safer:

 

* switching will occur only if the waiting greenlet is executing :meth:`get` method currently;

 

* any error raised in the greenlet is handled inside :meth:`switch` and :meth:`throw`

 

* if :meth:`switch`/:meth:`throw` is called before the receiver calls :meth:`get`, then :class:`Waiter`

 

will store the value/exception. The following :meth:`get` will return the value/raise the exception

 

简而言之,是对greenlet.greenlet类switch 和 throw函数的分装,用来存储返回值greenlet的返回值或者捕获在greenlet中抛出的异常。我们知道,在原生的greenlet中,如果一个greenlet抛出了异常,那么该异常将会展开至其parent greenlet

 

回到Hub.wait函数,第8行 watcher.start(waiter.switch, unique) 注册了一个回调,在一定时间(1s)之后调用回调函数waiter.switc.,注意,waiter.switch此时并没有执行。然后第10行调用waiter.get。看看这个get函数(gevent.hub.Waiter.get):

 

 1     def get(self):

 2         """If a value/an exception is stored, return/raise it. Otherwise until switch() or throw() is called."""

 3         if self._exception is not _NONE:

 4             if self._exception is None:

 5                 return self.value

 6             else:

 7                 getcurrent().throw(*self._exception)

 8         else:

 9             if self.greenlet is not None:

10                 raise ConcurrentObjectUseError('This Waiter is already used by %r' % (self.greenlet, ))

11             self.greenlet = getcurrent() # 存储当前协程,之后从hub switch回来的时候使用

12             try:

13                 return self.hub.switch() # switch到hub

14             finally:

15                 self.greenlet = None

核心的逻辑在第11到15行,11行中,getcurrent获取当前的greenlet(在这个测试代码中,是main greenlet,即最原始的greenlet),将其复制给waiter.greenlet。然后13switchhub,在greenlet回顾章节的第二条提到,greenlet.greenlet的子类需要重写run方法,当调用子类的switch时会调用到该run方法。Hubrun方法实现如下:

 

 1     def run(self):

 2         """

 3         Entry-point to running the loop. This method is called automatically

 4         when the hub greenlet is scheduled; do not call it directly.

 5

 6         :raises LoopExit: If the loop finishes running. This means

 7            that there are no other scheduled greenlets, and no active

 8            watchers or servers. In some situations, this indicates a

 9            programming error.

10         """

11         assert self is getcurrent(), 'Do not call Hub.run() directly'

12         while True:

13             loop = self.loop

14             loop.error_handler = self

15             try:

16                 loop.run()

17             finally:

18                 loop.error_handler = None  # break the refcount cycle

19             self.parent.throw(LoopExit('This operation would block forever', self))

loop自然是libev的事件循环。doc中提到,这个loop理论上会一直循环,如果结束,那么表明没有任何监听的事件(包括IO 定时等)。之前在Hub.wait函数中注册了定时器,那么在这个run中,如果时间到了,那么会调用定时器的callback,也就是之前的waiter.switch, 我们再来看看这个函数(gevent.hub.Waiter.switch):

 

 1     def switch(self, value=None):

 2         """Switch to the greenlet if one's available. Otherwise store the value."""

 3         greenlet = self.greenlet

 4         if greenlet is None:

 5             self.value = value

 6             self._exception = None

 7         else:

 8             assert getcurrent() is self.hub, "Can only use Waiter.switch method from the Hub greenlet"

 9             switch = greenlet.switch

10             try:

11                 switch(value)

12             except:

13                 self.hub.handle_error(switch, *sys.exc_info())

核心代码在第8到13行,第8行保证调用到该函数的时候一定再hub这个greenlet中,这是很自然的,因为这个函数一定是在Hub.run中被调用。第11switchwaiter.greenlet这个协程,在讲解waiter.get的时候就提到了waiter.greenletmain greenlet。注意,这里得switch会回到main greenlet被切出的地方(也就是main greenlet挂起的地方),那就是在waiter.get的第10行,整个逻辑也就恢复到main greenlet继续执行。

 

总结 : sleep的作用很简单,从当前greenlet.greenlet切换至Hub,超时之后再从hub切换到之前的greenlet继续执行。 通过这个例子可以知道,gevent将任何阻塞性的操作封装成一个Watcher,然后从调用阻塞操作的协程切换到Hub,等到阻塞操作完成之后,再从Hub切换到之前的协程。

 

gevent调度示例2

 

上面这个例子,虽然能够理顺gevent的调度流程,但事实上并没有体现出gevent 协作的优势。接下来看看 gevent tutorial 的例子:

 

 1 import gevent

 2

 3 def foo():

 4     print('Running in foo')

 5     gevent.sleep(0)

 6     print('Explicit context switch to foo again')

 7

 8 def bar():

 9     print('Explicit context to bar')

10     gevent.sleep(0)

11     print('Implicit context switch back to bar')

12

13 gevent.joinall([

14     gevent.spawn(foo),

15     gevent.spawn(bar),

16 ])

17

18 # output

19 Running in foo

20 Explicit context to bar

21 Explicit context switch to foo again

22 Implicit context switch back to bar

从输出可以看到, foo和bar依次输出,显然是在gevent.sleep的时候发生了执行流程切换,gevent.sleep再前面已经介绍了,那么这里主要关注spawnjoinall函数

 

gevent.spawn本质调用了gevent.greenlet.Greenlet的类方法spawn

 

1     @classmethod

2     def spawn(cls, *args, **kwargs):

3         g = cls(*args, **kwargs)

4         g.start()

5         return g

这个类方法调用了Greenlet的两个函数,__init__ 和 start. init函数中最为关键的是这段代码:

 

1     def __init__(self, run=None, *args, **kwargs):

2         greenlet.__init__(self, None, get_hub()) # 将新创生的greenlet实例的parent一律设置成hub

3

4         if run is not None:

5             self._run = run

start函数的定义也很简单(gevent.greenlet.Greenlet.start):

 

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)

注册回调事件self.switch到hub.loop,注意Greenlet.switch最终会调用到Greenlet._run, 也就是spawn函数传入的callable对象(foobar)。这里仅仅是注册,但还没有开始事件轮询,gevent.joinall就是用来启动事件轮询并等待运行结果的。

 

joinall函数会一路调用到gevent.hub.iwait函数:

 

 1 def iwait(objects, timeout=None, count=None):

 2     """

 3     Iteratively yield *objects* as they are ready, until all (or *count*) are ready

 4     or *timeout* expired.

 5     """

 6     # QQQ would be nice to support iterable here that can be generated slowly (why?)

 7     if objects is None:

 8         yield get_hub().join(timeout=timeout)

 9         return

10

11     count = len(objects) if count is None else min(count, len(objects))

12     waiter = _MultipleWaiter() # _MultipleWaiter是Waiter的子类

13     switch = waiter.switch

14

15     if timeout is not None:

16         timer = get_hub().loop.timer(timeout, priority=-1)

17         timer.start(switch, _NONE)

18

19     try:

20         for obj in objects:

21             obj.rawlink(switch) # 这里往hub.loop注册了回调

22  

23         for idx in xrange(count):

24             print 'for in iwait', idx

25             item = waiter.get() # 这里会切换到hub

26             print 'come here ', item, getcurrent()

27             waiter.clear()

28             if item is _NONE:

29                 return

30             yield item

31     finally:

32         if timeout is not None:

33             timer.stop()

34         for obj in objects:

35             unlink = getattr(obj, 'unlink', None)

36             if unlink:

37                 try:

38                     unlink(switch)

39                 except:

40                     traceback.print_exc()

然后iwait函数第23行开始的循环,逐个调用waiter.get。这里的waiter_MultipleWaiter(Waiter)的实例,其get函数最终调用到Waiter.get。前面已经详细介绍了Waiter.get,简而言之,就是switchhub。我们利用greenlettracing功能可以看到整个greenlet.greenletswitch流程,修改后的代码如下:

 

 1 import gevent

 2 import greenlet

 3 def callback(event, args):

 4     print event, args[0], '===:>>>>', args[1]

 5

 6 def foo():

 7     print('Running in foo')

 8     gevent.sleep(0)

 9     print('Explicit context switch to foo again')

10

11 def bar():

12     print('Explicit context to bar')

13     gevent.sleep(0)

14     print('Implicit context switch back to bar')

15

16 print 'main greenlet info: ', greenlet.greenlet.getcurrent()

17 print 'hub info', gevent.get_hub()

18 oldtrace = greenlet.settrace(callback)

19         

20 gevent.joinall([

21     gevent.spawn(foo),

22     gevent.spawn(bar),

23 ])

24 greenlet.settrace(oldtrace)

 

切换流程及原因见下图:

 

 

总结 :gevent.spawn创建一个新的Greenlet,并注册到hubloop上,调用gevent.joinall或者Greenlet.join的时候开始切换到hub

 

本文通过两个简单的例子并结合源码分析了gevent的协程调度流程。gevent的使用非常方便,尤其是在web server中,基本上应用App什么都不用做就能享受gevent带来的好处。笔者阅读gevent源码最重要的原因在于想了解geventgreenlet的封装和使用,greenlet很强大,强大到容易出错,而gevent保证在两层协程之间切换,值得借鉴!

 

来源:博客园

0 0
原创粉丝点击