Eventlet的基础
用官方的开场白介绍eventlet。eventlet是一个python并发(concurrent)网络库,能够在不改变编程方式的情况下,改变程序的运行方式。
- 使用epoll\kequeue\libevent实现高伸缩性的非堵塞I/O。
- 依靠协程(coroutines),可以类似threading库那样采用堵塞式的编程模型,反而获得非堵塞I/O的收益。
- 事件的分发(dispatch)是隐含式的,因此可以容易从底层使用Eventlet库,或者是作为大型程序的一个部分。
在我的理解中,Eventlet的神奇之处在于两点:
- 不改变编程模型,虽然底层是异步I/O,但是可以像堵塞式那样
正常
的编程,没有大量的嵌套回调。- 对于I/O事件是隐式分发的,就像使用threading库那样、甚至说比之还要方便;也无需大量的显式的协程调用,这点可以和tornado对比。
eventlet的底层greenlet
说起eventlet总也离不开其底层依赖的greenlet这个Python的协程库。这里不讨论greenlet的实现,只强调greenlet的几个特性。
协程的意思就是相互协助的程序。它与线程的区别是,协程之间的运行顺序是由程序本身确定的,而不是由内核决定;协程拥有自己独立的栈,可以随时终止和继续运行。
下面先提供一个实际的例子,然后分析greenlet的几个接口,再回过头说明这个例子的输出。
|
|
greenlet.greenlet
|
|
父协程有什么用?
父协程可以通过调用greenlet.getcurrent().parent
得到,那么这里传入父协程有什么用处?
简单的来说,父协程最大的用处在于,当前协程运行退出后,自动切换到父协程运行。换句话说,如果parent=None
那么创建的协程退出后将自动切回到创建该协程的调用者中运行。
|
|
我们可以看到child
的参数和运行结束。然后自动切换到了父协程中运行。需要注意的是,当子线程运行结束自动切换到父协程时,会自动专递一个None
参数。
为什么子协程退出切换到父协程时专递一个None参数
|
|
我们可以看到,当子协程自然退出后,父协程通过switch
获得了None
参数。想一想,如果子协程不传递None
参数,那么父协程就需要在switch
处报错了。
如果一个协程永远不被调用switch
会怎么样
一个协程如果没有被调用switch
,那么他就永远不能运行。这点和线程完全不同,线程由内核调度,如果主线程自然退出,那么其他线程依然运行(非daemon的线程)。但是,在协程里如果主协程退出,或者进程在其他协程中退出,那么整个程序退出,其他协程没有运行的机会了。看一个官方的例子:
|
|
我们看到7, 8
没有输出,因为g1的父协程是主协程,当g1运行结束后,自动切换到主协程,而主协程没有做任何动作就退出了。
greenlet.switch
|
|
greenlet.switch
会导致程序切换到被调用的协程中运行。例如前几个例子中的child.switch()
会切换到child
协程中运行。
协程如何继续运行?
当g.switch
被调用时,g
在何处继续运行有三种情况:
- 第一种情况,g没有运行,那么g在run入口处运行。传递的参数作为run的参数。
- 第二种情况,g已经运行过,那么g在上次运行停止,也就是调用了otehr.switch而被切换出去的地方继续运行。
- 第三种情况,g已经运行退出。那么g会直接返回,如果有参数传递,则直接返回参数
|
|
这里需要注意,当调用已经结束的协程时,会直接返回传递的参数或者是()
,而不是之前的None
。
greenlet.throw
|
|
throw
调用类似switch
调用,会立即转到调用的线程运行,并立即抛出异常,类似:
|
|
如果throw
没有参数,那么协程会抛出greenlet.GreenletExit
。这个异常不会向上传递给父协程,相当于正常的退出。
|
|
需要注意到,子协程内部产生异常,从而打印exit
。父协程获取到了子协程抛出的异常作为返回值,而不是继续抛出异常。所以,调用throw()
是安全中断协程的方法。
第一个例子的输出
|
|
从结果可知道,调用g.switch
后返回的参数,是下次切换到本协程(其他协程调用本协程的switch
或者throww
)传递的参数,而与协程g
没有关系。
协程与线程
假如在一个协程,切换到另一个线程中的协程会如何?
|
|
每个协程是依赖于栈空间的,而线程拥有独立的空间,当切换过去必然引起错误。greenlet
也不允许这种切换