當前位置:
首頁 > 知識 > Greenlet 詳解

Greenlet 詳解

(點擊

上方藍字

,快速關注我們)




來源:xybaby


www.cnblogs.com/xybaby/p/6337944.html


如有好文章投稿,請點擊 → 這裡了解詳情




本文內容主要來自對官網文檔的翻譯,在其中也加入了很多自己的理解和例子。主要包括以下內容:什麼是greenlet,greenlet的切換與函數調用的區別,greenlet的生命周期,以及使用greenlet的注意事項。



greenlet初體驗




Greenlet是python的一個C擴展,來源於Stackless python,旨在提供可自行調度的『微線程』, 即協程。generator實現的協程在yield value時只能將value返回給調用者(caller)。 而在greenlet中,target.switch(value)可以切換到指定的協程(target), 然後yield value。greenlet用switch來表示協程的切換,從一個協程切換到另一個協程需要顯式指定。




greenlet的安裝很簡單:pip install greenlet 即可,安裝好了之後我們來看一個官方的例子:





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

()




輸出為:





12 56 34




當創建一個greenlet時,首先初始化一個空的棧, switch到這個棧的時候,會運行在greenlet構造時傳入的函數(首先在test1中列印 12), 如果在這個函數(test1)中switch到其他協程(到了test2 列印34),那麼該協程會被掛起,等到切換回來(在test2中切換回來 列印34)。當這個協程對應函數執行完畢,那麼這個協程就變成dead狀態。




注意:上面沒有列印test2的最後一行輸出 78,因為在test2中切換到gr1之後掛起,但是沒有地方再切換回來。這個可能造成泄漏,後面細說。




greenlet module與class




我們首先看一下greenlet這個module裡面的屬性:





>>

dir

(

greenlet

)


[

"GREENLET_USE_GC"

,

"GREENLET_USE_TRACING"

,

"GreenletExit"

,

"_C_API"

,

"__doc__"

,

"__file__"

,

"__name__"

,

"__package__"

,

"__version__"

,

"error"

,

"getcurrent"

,

"gettrace"

,

"greenlet"

,

"settrace"

]


>>>




其中,比較重要的是getcurrent(), 類greenlet、異常類GreenletExit。




getcurrent()返回當前的greenlet實例;




GreenletExit:是一個特殊的異常,當觸發了這個異常的時候,即使不處理,也不會拋到其parent(後面會提到協程中對返回值或者異常的處理)




然後我們再來看看greenlet.greenlet這個類:





>>>

dir

(

greenlet

.

greenlet

)


[

"GreenletExit"

,

"__class__"

,

"__delattr__"

,

"__dict__"

,

"__doc__"

,

"__format__"

,

"__getattribute__"

,

"__getstate__"

,

"__hash__"

,

"__init__"

,

"__new__"

,

"__nonzero__"

,

"__reduce__"

,

"__reduce_ex__"

,

"__repr__"

,

"__setattr__"

,

  

"__sizeof__"

,

"__str__"

,

"__subclasshook__"

,

"_stack_saved"

,

"dead"

,

"error"

,

"getcurrent"

,

"gettrace"

,

"gr_frame"

,

"parent"

,

"run"

,

"settrace"

,

"switch"

,

"throw"

]


>>>




比較重要的幾個屬性:






  • run:當greenlet啟動的時候會調用到這個callable,如果我們需要繼承greenlet.greenlet時,需要重寫該方法



  • switch:前面已經介紹過了,在greenlet之間切換



  • parent:可讀寫屬性,後面介紹



  • dead:如果greenlet執行結束,那麼該屬性為true



  • throw:切換到指定greenlet後立即跑出異常




文章後面提到的greenlet大多都是指greenlet.greenlet這個class,請注意區別。




Switch not call




對於greenlet,最常用的寫法是 x = gr.switch(y)。 這句話的意思是切換到gr,傳入參數y。當從其他協程(不一定是這個gr)切換回來的時候,將值付給x。





import greenlet


def test1

(

x

,

y

)

:


    

z

=

gr2

.

switch

(

x

+

y

)


    

print

(

"test1 "

,

z

)


 


def test2

(

u

)

:


    

print

(

"test2 "

,

u

)


    

gr1

.

switch

(

10

)


 


gr1

=

greenlet

.

greenlet

(

test1

)


gr2

=

greenlet

.

greenlet

(

test2

)


print

gr1

.

switch

(

"hello"

,

" world"

)




輸出:





(『test2 『, 『hello world』)


(『test1 『, 10)


None




上面的例子,第12行從main greenlet切換到了gr1,test1第3行切換到了gs2,然後gr1掛起,第8行從gr2切回gr1時,將值(10)返回值給了 z。




每一個Greenlet都有一個parent,一個新的greenlet在哪裡創生,當前環境的greenlet就是這個新greenlet的parent。所有的greenlet構成一棵樹,其跟節點就是還沒有手動創建greenlet時候的」main」 greenlet(事實上,在首次import greenlet的時候實例化)。當一個協程 正常結束,執行流程回到其對應的parent;或者在一個協程中拋出未被捕獲的異常,該異常也是傳遞到其parent。學習python的時候,有一句話會被無數次重複」everything is oblect」, 在學習greenlet的調用中,同樣有一句話應該深刻理解,「switch not call」。





import greenlet


def test1

(

x

,

y

)

:


    

print id

(

greenlet

.

getcurrent

()),

id

(

greenlet

.

getcurrent

().

parent

)

# 40240272 40239952


    

z

=

gr2

.

switch

(

x

+

y

)


    

print

"back z"

,

z


 


def test2

(

u

)

:


    

print id

(

greenlet

.

getcurrent

()),

id

(

greenlet

.

getcurrent

().

parent

)

# 40240352 40239952


    

return

"hehe"


 


gr1

=

greenlet

.

greenlet

(

test1

)


gr2

=

greenlet

.

greenlet

(

test2

)


print id

(

greenlet

.

getcurrent

()),

id

(

gr1

),

id

(

gr2

)

    

# 40239952, 40240272, 40240352


print

gr1

.

switch

(

"hello"

,

" world"

),

"back to main"

    

# hehe back to main




上述例子可以看到,盡量是從test1所在的協程gr1 切換到了gr2,但gr2的parent還是』main』 greenlet,因為默認的parent取決於greenlet的創生環境。另外 在test2中return之後整個返回值返回到了其parent,而不是switch到該協程的地方(即不是test1),這個跟我們平時的函數調用不一樣,記住「switch not call」。對於異常 也是展開至parent。





import greenlet


def test1

(

x

,

y

)

:


    

try

:


        

z

=

gr2

.

switch

(

x

+

y

)


    

except

Exception

:


        

print

"catch Exception in test1"


 


def test2

(

u

)

:


    

assert

False


 


gr1

=

greenlet

.

greenlet

(

test1

)


gr2

=

greenlet

.

greenlet

(

test2

)


try

:


    

gr1

.

switch

(

"hello"

,

" world"

)


except

:


    

print

"catch Exception in main"




輸出為:





catch Exception in main




Greenlet生命周期




文章開始的地方提到第一個例子中的gr2其實並沒有正常結束,我們可以借用greenlet.dead這個屬性來查看:





from greenlet import greenlet


def test1

()

:


    

gr2

.

switch

(

1

)


    

print

"test1 finished"


 


def test2

(

x

)

:


    

print

"test2 first"

,

x


    

z

=

gr1

.

switch

()


    

print

"test2 back"

,

z


 


gr1

=

greenlet

(

test1

)


gr2

=

greenlet

(

test2

)


gr1

.

switch

()


print

"gr1 is dead?: %s, gr2 is dead?: %s"

%

(

gr1

.

dead

,

gr2

.

dead

)


gr2

.

switch

()


print

"gr1 is dead?: %s, gr2 is dead?: %s"

%

(

gr1

.

dead

,

gr2

.

dead

)


print

gr2

.

switch

(

10

)




輸出:





test2 first 1


test1 finished


gr1 is dead?: True, gr2 is dead?: False


test2 back ()


gr1 is dead?: True, gr2 is dead?: True


10




從這個例子可以看出:






  • 只有當協程對應的函數執行完畢,協程才會die,所以第一次Check的時候gr2並沒有die,因為第9行切換出去了就沒切回來。在main中再switch到gr2的時候, 執行後面的邏輯,gr2 die。



  • 如果試圖再次switch到一個已經是dead狀態的greenlet會怎麼樣呢,事實上會切換到其parent greenlet。




Greenlet Traceing




Greenlet也提供了介面使得程序員可以監控greenlet的整個調度流程。主要是gettrace 和 settrace(callback)函數。下面看一個例子:





def test_greenlet_tracing

()

:


    

def callback

(

event

,

args

)

:


        

print

event

,

"from"

,

id

(

args

[

0

]),

"to"

,

id

(

args

[

1

])


 


    

def dummy

()

:


        

g2

.

switch

()


 


    

def dummyexception

()

:


        

raise Exception

(

"excep in coroutine"

)


 


    

main

=

greenlet

.

getcurrent

()


    

g1

=

greenlet

.

greenlet

(

dummy

)


    

g2

=

greenlet

.

greenlet

(

dummyexception

)


    

print

"main id %s, gr1 id %s, gr2 id %s"

%

(

id

(

main

),

id

(

g1

),

id

(

g2

))


    

oldtrace

=

greenlet

.

settrace

(

callback

)


    

try

:


        

g1

.

switch

()


    

except

:


        

print

"Exception"


    

finally

:


        

greenlet

.

settrace

(

oldtrace

)


 


test_greenlet_tracing

()




輸出:





main id 40604416, gr1 id 40604736, gr2 id 40604816


switch from 40604416 to 40604736


switch from 40604736 to 40604816


throw from 40604816 to 40604416


Exception




其中callback函數event是switch或者throw之一,表明是正常調度還是異常跑出;args是二元組,表示是從協程args[0]切換到了協程args[1]。上面的輸出展示了切換流程:從main到gr1,然後到gr2,最後回到main。




greenlet使用建議:




使用greenlet需要注意一下三點:




第一:greenlet創生之後,一定要結束,不能switch出去就不回來了,否則容易造成內存泄露。




第二:python中每個線程都有自己的main greenlet及其對應的sub-greenlet ,不能線程之間的greenlet是不能相互切換的。




第三:不能存在循環引用,這個是官方文檔明確說明。





  」Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet』s frames will not be detected. 「




對於第一點,我們來看一個例子:





from greenlet import

greenlet

,

GreenletExit


huge

=

[]


def show_leak

()

:


    

def test1

()

:


        

gr2

.

switch

()


 


    

def test2

()

:


        

huge

.

extend

([

x*

x

for

x

in

range

(

100

)])


        

gr1

.

switch

()


        

print

"finish switch del huge"


        

del

huge

[

:

]


    


    

gr1

=

greenlet

(

test1

)


    

gr2

=

greenlet

(

test2

)


    

gr1

.

switch

()


    

gr1

=

gr2

=

None


    

print

"length of huge is zero ? %s"

%

len

(

huge

)


 


if

__name__

==

"__main__"

:


    

show_leak

()


   

# output: length of huge is zero ? 100




在test2函數中 第11行,我們將huge清空,然後再第16行將gr1、gr2的引用計數降到了0。但運行結果告訴我們,第11行並沒有執行,所以如果一個協程沒有正常結束是很危險的,往往不符合程序員的預期。greenlet提供了解決這個問題的辦法,官網文檔提到:

如果一個greenlet實例的引用計數變成0,那麼會在上次掛起的地方拋出GreenletExit異常,這就使得我們可以通過try … finally 處理資源泄露的情況。

如下面的代碼:





from greenlet import

greenlet

,

GreenletExit


huge

=

[]


def show_leak

()

:


    

def test1

()

:


        

gr2

.

switch

()


 


    

def test2

()

:


        

huge

.

extend

([

x*

x

for

x

in

range

(

100

)])


        

try

:


            

gr1

.

switch

()


        

finally

:


            

print

"finish switch del huge"


            

del

huge

[

:

]


    


    

gr1

=

greenlet

(

test1

)


    

gr2

=

greenlet

(

test2

)


    

gr1

.

switch

()


    

gr1

=

gr2

=

None


    

print

"length of huge is zero ? %s"

%

len

(

huge

)


 


if

__name__

==

"__main__"

:


    

show_leak

()


    

# output :


    

# finish switch del huge


 

  

# length of huge is zero ? 0




上述代碼的switch流程:main greenlet –> gr1 –> gr2 –> gr1 –> main greenlet, 很明顯gr2沒有正常結束(在第10行颳起了)。第18行之後gr1,gr2的引用計數都變成0,那麼會在第10行拋出GreenletExit異常,因此finally語句有機會執行。同時,在文章開始介紹Greenlet module的時候也提到了,GreenletExit這個異常並不會拋出到parent,所以main greenlet也不會出異常。




看上去貌似解決了問題,但這對程序員要求太高了,百密一疏。所以最好的辦法還是保證協程的正常結束。




總結:




之前的文章其實已經提到提到了coroutine協程的強大之處,對於非同步非阻塞,而且還需要保留上下文的場景非常適用。greenlet跟強大,可以從一個協程切換到任意其他協程,這是generator做不到的,但這種能力其實也是雙刃劍,前面的注意事項也提到了,必須保證greenlet的正常結束,在協程之間任意的切換很容易出問題。




比如對於服務之間非同步請求的例子,簡化為服務A的一個函數foo需要非同步訪問服務B,可以這樣封裝greenlet:用decorator裝飾函數foo,當調用這個foo的時候建立一個greenlet實例,並為這個greenley對應一個唯一的gid,在foo方法發出非同步請求(寫到gid)之後,switch到parent,這個時候這個新的協程處於掛起狀態。當請求返回之後,通過gid找到之前被掛起的協程,恢復該協程即可。More simple More safety,保證旨在main和一級子協程之間切換。需要注意的是處理各種異常 以及請求超時的情況,避免內存泄露,gvent對greenlet的使用大致也是這樣的。




參考






  • http://www.cnblogs.com/xybaby/p/6323358.html



  • https://pypi.python.org/pypi/greenlet



  • https://en.wikipedia.org/wiki/Stackless_Python



  • http://greenlet.readthedocs.io/en/latest/



  • http://stackoverflow.com/questions/715758/coroutine-vs-continuation-vs-generator




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

Python標準庫:itertools模塊
矽谷網紅課!19周成為最搶手的深度學習技術人才
Python vs Ruby: 誰是最好的 web 開發語言?
學慣用 Python 編程時要避免的 3 個錯誤

TAG:Python開發者 |

您可能感興趣

python greenlet 背景介紹與實現機制
用 greenlet 實現 Python 中的並發