當前位置:
首頁 > 知識 > 用 greenlet 實現 Python 中的並發

用 greenlet 實現 Python 中的並發

點擊上方「

Python開發

」,選擇「置頂公眾號」


關鍵時刻,第一時間送達!






回顧一下,協程可以在一個函數執行過程中將其掛起,去執行另一個函數,並在必要時將之前的函數喚醒。在Python的語言環境里,協程是相當常用的實現「並發」的方法。上一篇的例子中,我們演示了如何使用yield關鍵字來實現協程,不過這個看上去非常不直觀。這裡我們要介紹一個非常好用的框架greenlet,很多知名的網路並發框架如eventlet,gevent都是基於它實現的。



第一個例子




沿襲我們一直以來的習慣,先從例子開始,這次偷個懶,直接把官方文檔中的例子拿過來:





from

greenlet

import

greenlet



def

test1

()

:


    

print

12


    

gr2

.

switch

()


    

print

34



def

test2

()

:

    

print

56


    

gr1

.

switch

()


    

print

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

()

:


    

print

12


    

gr2

.

switch

()


    

print

34



def

test2

()

:


    

print

56



gr1

=

greenlet

(

test1

)


gr2

=

greenlet

(

test2

,

gr1

)


gr1

.

switch

()


print

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

()

:


    

print

12


    

gr2

.

switch

()


    

print

34


    

gr2

.

switch

()




大家可能會感覺到父子協程之間的關係,就像函數調用一樣,一個嵌套一個。的確,其實greenlet協程的實現就是使用了棧,其運行的上下文保存在棧中,」main」主協程處於棧底的位置,而當前運行中的協程就在棧頂。這同函數是一樣。此外,在任何時候,你都可以使用」greenlet.getcurrent()」,獲取當前運行中的協程對象。比如在函數test2()中執行」greenlet.getcurrent()」,其返回就等於」gr2″。




異常




既然協程是存放在棧中,那一個協程要拋出異常,就會先拋到其父協程中,如果所有父協程都不捕獲此異常,程序才會退出。我們試下,把上面的例子中函數test2()的代碼改為:





def

test2

()

:


    

print

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

()

:


    

print

12


    

try

:


        

gr2

.

switch

()


    

except

NameError

:


        

print

90


    

print

34




運行後的結果,如果」gr2″的父協程是」gr1″,則異常被捕獲,並列印90。否則,異常會被拋出。以上實驗很好的證明了,子協程拋出的異常會根據棧里的順序,依次拋到父協程里。




有一個異常是特例,不會被拋到父協程中,那就是」greenlet.GreenletExit」,這個異常會讓當前協程強制退出。比如,我們將函數test2()改為:





def

test2

()

:


    

print

56


    

raise

greenlet

.

GreenletExit


    print

78




那代碼行」print 78″永遠不會被執行。但這個異常不會往上拋,所以其父協程還是可以正常運行。




另外,我們可以通過greenlet對象的」throw()」方法,手動往一個協程里拋個異常。比如,我們在test1()里調一個throw()方法:





def

test1

()

:


    

print

12


    

gr2

.

throw

(

NameError

)


    

try

:


        

gr2

.

switch

()


    

except

NameError

:


        

print

90


    

print

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

()

:


    

print

12


    

y

=

gr2

.

switch

(

56

)


    

print

y



def

test2

(

x

)

:


    

print

x


    

gr1

.

switch

(

34

)


    

print

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

:


            

print

"Consume %s"

%

receival


            

last

=

receival



def

producer

(

n

)

:


    

con

.

switch

()


    

x

=

0


    

while

x

<

n

:


        

x

+=

1


        

print

"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 正則表達式
Python 標準庫之 collections 使用教程

TAG:Python開發 |