Gevent 排程流程解析

發表於2017-07-25

gevent是目前應用非常廣泛的網路庫,高效的輪詢IO庫libev加上greenlet實現的協程(coroutine),使得gevent的效能非常出色,尤其是在web應用中。本文介紹gevent的排程流程,主要包括gevent對greenlet的封裝和使用,以及greenlet與libev的協作。閱讀本文需要對greenlet有一定的認識,可以參考這篇文章,另外,本文分析的gevent版本為1.2,可以通過gevent.version_info檢視版本號。

gevent簡介:

gevent是基於協程(greenlet)的網路庫,底層的事件輪詢基於libev(早期是libevent),gevent的API概念和Python標準庫一致(如事件,佇列)。gevent有一個很有意思的東西-monkey-patch,能夠使python標準庫中的阻塞操作變成非同步,如socket的讀寫。

gevent來源於eventlet,自稱比後者實現更簡單、API更方便且效能更好,許多開源的web伺服器也使用了gevent,如gunicorn、paste,當然gevent本生也可以作為一個python web伺服器使用。這篇文章對常見的wsgi server進行效能對比,gevent不管在http1.0還是http1.1都表現非常出色。下圖是目前常用的http1.1標準下的表現:1089769-20170206161920057-404494008

gevent高效的祕訣就是greenlet和libev啦,greenlet在之前的博文有介紹,gevent對greenlet的使用比較限制,只能在兩層協程之間切換,簡單也不容易出錯。libev使用輪訓非阻塞的方式進行事件處理,比如unix下的epoll。早期gevent使用libevent,後來替換成libev,因為libev“提供更少的核心功能以求更改的效率”,這裡有libev和libevent的效能對比

1089769-20170206161920057-404494008

greenlet回顧:

如果想了解gevent的排程流程,最重要的是對greenlet有基本的瞭解。下面總結一些個人認為比較重要的點:

  1. 每一個greenlet.greenlet例項都有一個parent(可指定,預設為創生新的greenlet.greenlet所在環境),當greenlet.greenlet例項執行完邏輯正常結束、或者丟擲異常結束時,執行邏輯切回到其parent。
  2. 可以繼承greenlet.greenlet,子類需要實現run方法,當呼叫greenlet.switch方法時會呼叫到這個run方法

在gevent中,有兩個類繼承了greenlet.greenlet,分別是gevent.hub.Hub和gevent.greenlet.Greenlet。後文中,如果是greenlet.greenlet這種寫法,那麼指的是原生的類庫greentlet,如果是greenlet(或者Greenlet)那麼指gevent封裝後的greenlet。

greenlet排程流程:

首先,給出總結性的結論,後面再結合例項和原始碼一步步分析。

每個gevent執行緒都有一個hub,前面提到hub是greenlet.greenlet的例項。hub例項在需要的時候創生(Lazy Created),那麼其parent是main greenlet。之後任何的Greenlet(注意是greenlet.greenlet的子類)例項的parent都設定成hub。hub呼叫libev提供的事件迴圈來處理Greenlet代表的任務,當Greenlet例項結束(正常或者異常)之後,執行邏輯又切換到hub。

gevent排程示例1:

我們看下面最簡單的程式碼:

上面的程式碼很簡單,但事實上gevent的核心都包含在其中,接下來結合原始碼進行分析。

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

首先是獲取hub(第2行),然後在hub上wait這個定時器事件(第9行)。get_hub原始碼如下(gevent.hub.get_hub):

可以看到,hub是執行緒內唯一的,之前也提到過greenlet是執行緒獨立的,每個執行緒有各自的greenlet棧。hubtype預設就是gevent.hub.Hub,在hub的初始化函式(__init__)中,會建立loop屬性,預設也就是libev的python封裝。

回到sleep函式定義,hub.wait(loop.timer(seconds, ref=ref))。hub.wait函式非常關鍵,對於任何阻塞性操作,比如timer、io都會呼叫這個函式,其作用一句話概括:從當前協程切換到hub,直到watcher對應的事件就緒再從hub切換回來。wait函式原始碼如下(gevent.hub.Hub.wait):

形參watcher就是loop.timer例項,其cython描述在corecext.pyx,我們簡單理解成是一個定時器事件就行了。上面的程式碼中,建立了一個Waiter(gevent.hub.Waiter)物件,這個物件起什麼作用呢,這個類的doc寫得非常清楚:

簡而言之,是對greenlet.greenlet類switch 和 throw函式的分裝,用來儲存返回值greenlet的返回值或者捕獲在greenlet中丟擲的異常。我們知道,在原生的greenlet中,如果一個greenlet丟擲了異常,那麼該異常將會展開至其parent greenlet。

回到Hub.wait函式,第8行 watcher.start(waiter.switch, unique) 註冊了一個回撥,在一定時間(1s)之後呼叫回撥函式waiter.switch。注意,waiter.switch此時並沒有執行。然後第10行呼叫waiter.get。看看這個get函式(gevent.hub.Waiter.get):

核心的邏輯在第11到15行,11行中,getcurrent獲取當前的greenlet(在這個測試程式碼中,是main greenlet,即最原始的greenlet),將其複製給waiter.greenlet。然後13行switch到hub,在greenlet回顧章節的第二條提到,greenlet.greenlet的子類需要重寫run方法,當呼叫子類的switch時會呼叫到該run方法。Hub的run方法實現如下:

loop自然是libev的事件迴圈。doc中提到,這個loop理論上會一直迴圈,如果結束,那麼表明沒有任何監聽的事件(包括IO 定時等)。之前在Hub.wait函式中註冊了定時器,那麼在這個run中,如果時間到了,那麼會呼叫定時器的callback,也就是之前的waiter.switch, 我們再來看看這個函式(gevent.hub.Waiter.switch):

核心程式碼在第8到13行,第8行保證呼叫到該函式的時候一定在hub這個協程中,這是很自然的,因為這個函式一定是在Hub.run中被呼叫。第11行switch到waiter.greenlet這個協程,在講解waiter.get的時候就提到了waiter.greenlet是main greenlet。注意,這裡得switch會回到main greenlet被切出的地方(也就是main greenlet掛起的地方),那就是在waiter.get的第10行,整個邏輯也就恢復到main greenlet繼續執行。

總結:sleep的作用很簡單,觸發一個阻塞的操作,導致呼叫hub.wait,從當前greenlet.greenlet切換至Hub,超時之後再從hub切換到之前的greenlet繼續執行通過這個例子可以知道,gevent將任何阻塞性的操作封裝成一個Watcher,然後從呼叫阻塞操作的協程切換到Hub,等到阻塞操作完成之後,再從Hub切換到之前的協程

gevent排程示例2:

上面這個例子,雖然能夠理順gevent的排程流程,但事實上並沒有體現出gevent 協作的優勢。接下來看看gevent tutorial的例子:

從輸出可以看到, foo和bar依次輸出,顯然是在gevent.sleep的時候發生了執行流程切換,gevent.sleep再前面已經介紹了,那麼這裡主要關注spawn和joinall函式。

gevent.spawn本質呼叫了gevent.greenlet.Greenlet的類方法spawn:

這個類方法呼叫了Greenlet的兩個函式,__init__ 和 start. init函式中最為關鍵的是這段程式碼:

start函式的定義也很簡單(gevent.greenlet.Greenlet.start):

註冊回撥事件self.switch到hub.loop,注意Greenlet.switch最終會呼叫到Greenlet._run, 也就是spawn函式傳入的callable物件(foo、bar)。這裡僅僅是註冊,但還沒有開始事件輪詢,gevent.joinall就是用來啟動事件輪詢並等待執行結果的。

joinall函式會一路呼叫到gevent.hub.iwait函式:

然後iwait函式第23行開始的迴圈,逐個呼叫waiter.get。這裡的waiter是_MultipleWaiter(Waiter)的例項,其get函式最終呼叫到Waiter.get。前面已經詳細介紹了Waiter.get,簡而言之,就是switch到hub。我們利用greenlet的tracing功能可以看到整個greenlet.greenlet的switch流程,修改後的程式碼如下:

切換流程及原因見下圖:Gevent 排程流程解析

總結:gevent.spawn建立一個新的Greenlet,並註冊到hub的loop上,呼叫gevent.joinall或者Greenlet.join的時候開始切換到hub。

本文通過兩個簡單的例子並結合原始碼分析了gevent的協程排程流程。gevent的使用非常方便,尤其是在web server中,基本上應用App什麼都不用做就能享受gevent帶來的好處。筆者閱讀gevent原始碼最重要的原因在於想了解gevent對greenlet的封裝和使用,greenlet很強大,強大到容易出錯,而gevent保證在兩層協程之間切換,值得借鑑!

參考

  • http://www.cnblogs.com/xybaby/p/6337944.html
  • http://www.gevent.org/
  • https://pypi.python.org/pypi/greenlet
  • http://software.schmorp.de/pkg/libev.html
  • http://libevent.org/
  • http://eventlet.net/
  • http://nichol.as/benchmark-of-python-web-servers
  • http://libev.schmorp.de/bench.html
  • http://sdiehl.github.io/gevent-tutorial/

相關文章