Eventlet的Hub
所有基于epoll
(多路复用)的框架,总有一个事件循环处理中心,一般称为Hub
。Eventlet
中的Hub
不仅是所有事件监听和处理的中心,也是greenlet调度的中心。下面看看Hub的事件处理逻辑。
|
|
定时事件
总体来说,Hub处理两件事件。第一类是定时事件,每次循环中处理到期的事件。上面我们可以看到,Hub每次循环将next_timer
中的定时事件进行排序,然后处理到期的定时事件。那么定时事件是如何添加到Hub中的呢?
|
|
通过add_timer
函数,将Timer
的实例加入next_timers
列表中。注意Timer
中的时间是到期的相对时间长度,而Hub
中记录的是绝对时间戳,因此add_timer
做了一个转换。定时事件处理的逻辑:
- 每次循环处理到期的定时事件
- 处理的顺序按照到期的事件由小到大处理
- 相同时间戳的事件按照添加的顺序(内存id)从小到大处理
监听事件
|
|
可以看到,对第二类监听事件的处理是通过epoll,每次循环返回一批满足条件的事件,然后依次执行回调函数。那么,监听事件是如何添加到Hub中的呢?
|
|
通过继承链Hub->BaseHub
,将监听事件添加到epoll
中并且在每次循环中处理满足监听条件的事件,执行回调函数。我们知道,greenlet
本身是不负责调度的,所有的调度需要由应用程序负责,那么eventlet
如何进行程序的调度?我们先从eventlet
定义的协程说起。
Eventlet的GreenThread
Eventlet通过继承greenlet.geenlet
自定义了一个绿色线程,实际上是一个协程。Eventlet
扩展了greenlet.greenlet
的语义:
wait
, 当被调用时会自动切换到其他协程上去,条件满足时再切回来link
, 注册协程退出时执行的清理函数kill
, 杀掉协程,但是在杀掉之前执行一次调度
|
|
使用协程的入口spawn
和spawn_n
一般使用eventlet
会有两个入口函数:
spawn
, eventlet.spawn ->eventlet.greenthread.spawn, 创建一个绿色线程并且返回这个绿色协程,并安排调度执行。spawn_n
, eventlet.spawn_n ->eventlet.greenthread.spawn_n, 创建一个greenlet,并返回,同时安排调度执行。
spawn
使用了自己的GreenThread
,而spawn_n
使用的是greenlet
,因而后者更加快速一点。
|
|
可以看到两个都调用了schedule_call_global
然后返回。这个函数会安排绿色协程的调度。
绿色线程如何调度
在创建GreenThread时,会通过调用不同的Hub
方法进行调度,在linux环境下两个函数一样,就以上面说到的schedule_call_global
为例说明。
|
|
可以看到,当创建绿色线程时通过调用scheduler_call_global
方法,然后再设置定时任务的方式添加入Hub进行调度。每个seconds=0
,即在每次循环处理定时任务的时候处理。一直到这里,并没有谈到监听事件是何时注册到epoll
进行监听的。我们通过socket
创建和监听为例,看一下整个过程是如何的。
socket如何创建、设置、注册和监听
原生的实现不兼容eventlet
,所以eventlet
对一些标准库进行了绿化
。用自己的实现替代了原生的实现。这里的不兼容主要体现在两点:
- 需要将堵塞模式设置为非堵塞,不然多路复用没有意义
- 需要将fd添加到epoll中进行监听,并且在满足条件下调度执行
通过GreenSocket实现探查下如何解决上面两个问题:
|
|
|
|
从上面的代码可以看到,使用了绿色线程的执行步骤是:
|
|
因此,我们可以知道eventlet
解决上面两个问题的方法:
- 封装原生socket,设置为非堵塞模式
- 在accept返回失败时,通过trampoline将其添加到Hub进行事件监听。
eventlet的绿化逻辑
可以归纳出eventlet
的大致处理逻辑:
- 调用spawn类函数创建一个绿色线程,通过Timer提交给Hub,并将switch函数设置为回调。在每次Hub循环处理Timer时,执行switch切回绿色线程运行;
- 通过绿化原生标准库,设置fd为非堵塞模式;在运行非堵塞accept、read、write等失败时trampoline,从而添加到hub中进行事件监听。等待下次Hub循环时通过epoll检查条件是否满足,然后再切换回绿色线程进行对应的操作。