用 greenlet 實現 Python 中的並發
點擊上方「
Python開發
」,選擇「置頂公眾號」
關鍵時刻,第一時間送達!
回顧一下,協程可以在一個函數執行過程中將其掛起,去執行另一個函數,並在必要時將之前的函數喚醒。在Python的語言環境里,協程是相當常用的實現「並發」的方法。上一篇的例子中,我們演示了如何使用yield關鍵字來實現協程,不過這個看上去非常不直觀。這裡我們要介紹一個非常好用的框架greenlet,很多知名的網路並發框架如eventlet,gevent都是基於它實現的。
第一個例子
沿襲我們一直以來的習慣,先從例子開始,這次偷個懶,直接把官方文檔中的例子拿過來:
from
greenlet
import
greenlet
def
test1
()
:
12
gr2
.
switch
()
34
def
test2
()
:
56
gr1
.
switch
()
78
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
)
gr1
.
switch
()
這裡創建了兩個greenlet協程對象,gr1和gr2,分別對應於函數test1()和test2()。使用greenlet對象的switch()方法,即可以切換協程。上例中,我們先調用」gr1.switch()」,函數test1()被執行,然後列印出」12″;接著由於」gr2.switch()」被調用,協程切換到函數test2(),列印出」56″;之後」gr1.switch()」又被調用,所以又切換到函數test1()。但注意,由於之前test1()已經執行到第5行,也就是」gr2.switch()」,所以切換回來後會繼續往下執行,也就是列印」34″;現在函數test1()退出,同時程序退出。由於再沒有」gr2.switch()」來切換至函數test2(),所以程序第11行」print 78″不會被執行。
所以,程序運行下來的輸出就是:
12
56
34
很好理解吧。使用switch()方法切換協程,也比」yield」, 「next/send」組合要直觀的多。上例中,我們也可以看出,greenlet協程的運行,其本質是串列的,所以它不是真正意義上的並發,因此也無法發揮CPU多核的優勢,不過,這個可以通過協程+進程組合的方式來解決,本文就不展開了。另外要注意的是,在沒有進行顯式切換時,部分代碼是無法被執行到的,比如上例中的」print 78″。
父子關係
創建協程對象的方法其實有兩個參數」greenlet(run=None, parent=None)」。參數」run」就是其要調用的方法,比如上例中的函數test1()和test2();參數」parent」定義了該協程對象的父協程,也就是說,greenlet協程之間是可以有父子關係的。如果不設或設為空,則其父協程就是程序默認的」main」主協程。這個」main」協程不需要用戶創建,它所對應的方法就是主程序,而所有用戶創建的協程都是其子孫。大家可以把greenlet協程集看作一顆樹,樹的根節點就是」main」,上例中的」gr1″和」gr2″就是其兩個位元組點。
在子協程執行完畢後,會自動返回父協程。比如上例中test1()函數退出,代碼會返回到主程序。讓我們寫個更清晰的例子來實驗下:
from
greenlet
import
greenlet
def
test1
()
:
12
gr2
.
switch
()
34
def
test2
()
:
56
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
,
gr1
)
gr1
.
switch
()
78
這裡創建greenlet對象」gr2″時,指定了其父協程是」gr1″。所以在函數test2()里,雖然沒有」gr1.switch()」代碼,但是在其退出後,程序一樣回到了函數test1(),並且執行」print 34″。同樣,在test1()退出後,代碼回到了主程序,並執行」print 78″。所以,最後的輸出就是:
12
56
34
78
如果上例中,」gr2″的父協程不是」gr1″而是」main」的話,那test2()運行完畢就會回到主程序並直接列印」78″,這樣」print 34″就不會執行。大家可以試一試。
還有一個重要的點,就是協程退出後,就無法再被執行了。如果上例在函數test1()中,再加一句」gr2.switch()」,運行的結果是一樣的。因為第二次調用」gr2.switch()」,什麼也不會運行。
def
test1
()
:
12
gr2
.
switch
()
34
gr2
.
switch
()
大家可能會感覺到父子協程之間的關係,就像函數調用一樣,一個嵌套一個。的確,其實greenlet協程的實現就是使用了棧,其運行的上下文保存在棧中,」main」主協程處於棧底的位置,而當前運行中的協程就在棧頂。這同函數是一樣。此外,在任何時候,你都可以使用」greenlet.getcurrent()」,獲取當前運行中的協程對象。比如在函數test2()中執行」greenlet.getcurrent()」,其返回就等於」gr2″。
異常
既然協程是存放在棧中,那一個協程要拋出異常,就會先拋到其父協程中,如果所有父協程都不捕獲此異常,程序才會退出。我們試下,把上面的例子中函數test2()的代碼改為:
def
test2
()
:
56
raise
NameError
程序執行後,我們可以看到Traceback信息:
File
"parent.py"
,
line
14
,
in
gr1
.
switch
()
File
"parent.py"
,
line
5
,
in
test1
gr2
.
switch
()
File
"parent.py"
,
line
10
,
in
test2
raise
NameError
同時大家可以試下,如果將」gr2″的父協程設為空,Traceback信息就會變為:
File
"parent.py"
,
line
14
,
in
gr1
.
switch
()
File
"parent.py"
,
line
10
,
in
test2
raise
NameError
因此,如果」gr2″的父協程是」gr1″的話,異常先回拋到函數test1()的代碼」gr2.switch()」處。所以,我們再對函數test1()改動下:
def
test1
()
:
12
try
:
gr2
.
switch
()
except
NameError
:
90
34
運行後的結果,如果」gr2″的父協程是」gr1″,則異常被捕獲,並列印90。否則,異常會被拋出。以上實驗很好的證明了,子協程拋出的異常會根據棧里的順序,依次拋到父協程里。
有一個異常是特例,不會被拋到父協程中,那就是」greenlet.GreenletExit」,這個異常會讓當前協程強制退出。比如,我們將函數test2()改為:
def
test2
()
:
56
raise
greenlet
.
GreenletExit
78
那代碼行」print 78″永遠不會被執行。但這個異常不會往上拋,所以其父協程還是可以正常運行。
另外,我們可以通過greenlet對象的」throw()」方法,手動往一個協程里拋個異常。比如,我們在test1()里調一個throw()方法:
def
test1
()
:
12
gr2
.
throw
(
NameError
)
try
:
gr2
.
switch
()
except
NameError
:
90
34
這樣,異常就會被拋出,運行後的Trackback是這樣的:
File
"exception.py"
,
line
21
,
in
gr1
.
switch
()
File
"exception.py"
,
line
5
,
in
test1
gr2
.
throw
(
NameError
)
如果將」gr2.throw(NameError)」放在」try」語句中,那該異常就會被捕獲,並列印」90″。另外,當」gr2″的父協程不是」gr1″而是」main」時,異常會直接拋到主程序中,此時函數test1()中的」try」語句就不起作用了。
協程間傳遞消息
在介紹生成器時,我們聊過可以使用生成器的send()方法來傳遞參數。greenlet也同樣支持,只要在其switch()方法調用時,傳入參數即可。我們再來基於本文第一個例子改造下:
from
greenlet
import
greenlet
def
test1
()
:
12
y
=
gr2
.
switch
(
56
)
y
def
test2
(
x
)
:
x
gr1
.
switch
(
34
)
78
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
)
gr1
.
switch
()
在test1()中調用」gr2.switch()」,由於協程」gr2″之前未被啟動,所以傳入的參數」56″會被賦在test2()函數的參數」x」上;在test2()中調用」gr1.switch()」,由於協程」gr1″之前已執行到第5行」y = gr2.switch(56)」這裡,所以傳入的參數」34″會作為」gr2.switch(56)」的返回值,賦給變數」y」。這樣,兩個協程之間的互傳消息就實現了。
讓我們將上一篇介紹生成器時寫的生產者消費者的例子,改為greenlet實現吧:
from
greenlet
import
greenlet
def
consumer
()
:
last
=
""
while
True
:
receival
=
pro
.
switch
(
last
)
if
receival
is
not
None
:
"Consume %s"
%
receival
last
=
receival
def
producer
(
n
)
:
con
.
switch
()
x
=
0
while
x
<
n
:
x
+=
1
"Produce %s"
%
x
last
=
con
.
switch
(
x
)
pro
=
greenlet
(
producer
)
con
=
greenlet
(
consumer
)
pro
.
switch
(
5
)
更多參考資料
greenlet的官方文檔
greenlet的源碼
本文中的示例代碼可以在這裡下載。
來源:思誠之道
www.bjhee.com/greenlet.html
Python開發整理髮布,轉載請聯繫作者獲
得授權
【點擊成為Java大神】
※Python 正則表達式
※Python 標準庫之 collections 使用教程
TAG:Python開發 |