您所在的位置:小祥子 » 编程 » Python » 正文

sleep函数——Gevent源码分析

时间:2015-07-04 编辑:Eric_Nirvana 来源:CnBlogs

gevent是一个异步I/O框架,当遇到I/O操作的时候,会自动切换任务,从而能异步地完成I/O操作
但是在测试的情况下,可以使用sleep函数来让gevent进行任务切换。示例如下:

import gevent

def test(id):
    print('Test %s is running...' % id)
    gevent.sleep(0)
    print('Test %s is done!' % id)

gevent.joinall([gevent.spawn(test, i) for i in range(2)])

该函数的执行结果是:

Test 0 is running...
Test 1 is running...
Test 0 is done!
Test 1 is done!

可见,sleep函数能让gevent切换协程,进行异步操作。
这次我想探究一下sleep函数的原理。

在了解sleep函数之前,我们需要了解一下gevent的运行
在前面的文章中,我们知道了gevent有个主协程hub的概念,当需要切换协程的时候,需要先回到hub,然后再由hub去切换。
其实主协程hub是一个特殊的协程Greenlet
当gevent运行的时候,gevent需要先创建一个主协程hub,并运行hub的run函数(具体源码在hub.py/run),比较简单,核心代码是loop.run(),这个run函数是Greenlet类中的run函数,用来切入loop中的子协程,源码在greenlet.py/run中。核心就是result = self._run(*self.args, **self.kwargs), _run函数用来执行这个子协程的任务

sleep函数

在刚刚的示例代码中,在sleep处设置断点,进行跟踪。
首先,进入sleep函数,函数在hub.py中:

def sleep(seconds=0, ref=True):
    hub = get_hub()	#获得主协程hub对象
    loop = hub.loop		#获得主循环
    if seconds <= 0:
        waiter = Waiter()
        loop.run_callback(waiter.switch)	#设置回调函数(即下次本协程执行的地点)
        waiter.get()
    else:
        hub.wait(loop.timer(seconds, ref=ref))

当seconds=0的时候,loop.run_callback(waiter.switch)把当前greenlet的switch注册到loop中,设置为回调函数,此时的loop是主协程hub下的loop。

sleep函数中最后调用了waiter.get()get函数简化如下:

def get(self):
	assert self.greenlet is None, 'This Waiter is already used by %r' % (self.greenlet, )
	self.greenlet = getcurrent()
	try:
		return self.hub.switch()
	finally:
		self.greenlet = None

self.greenlet = getcurrent(): 把greenlet设置为当前协程greenlet
return self.hub.switch(): 切换到主线程hub的主循环, 然后主循环再切换到下一个greenlet协程
工作流程如图:

总结

Gevent的工作原理(省略了执行完协程之后的过程)如下:

  1. 程序启动,需要创建主协程hub
  2. 主协程执行hub.run()函数,里面主要是执行loop.run(),loop中是子协程,相当于执行子协程的run()函数
  3. 切换到子协程,执行Greenlet.run()函数
  4. Greenlet.run()函数中,执行到self._run()函数,即执行该协程的任务,本例中为自己定义的test()函数
  5. 一直执行到sleep(0)语句
  6. 在sleep()函数中保存回调的位置(即保存该协程执行到的地方),调用waiter.get()函数
  7. waiter.get()函数将调用self.hub.switch()切回主协程hub
  8. hub.switch()将调用greenlet.switch()函数:

    1. 如果即将切换的协程未执行过run函数,则执行run函数;
    2. 如果执行过run函数,则调用Waiter.switch()函数接着上次执行的地方执行

重复以上的过程,直至所有协程任务全部执行完毕

关键词:函数 源码