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
()
:
12
gr2
.
switch
()
34
def
test2
()
:
56
gr1
.
switch
()
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
)
(
"test1 "
,
z
)
def test2
(
u
)
:
(
"test2 "
,
u
)
gr1
.
switch
(
10
)
gr1
=
greenlet
.
greenlet
(
test1
)
gr2
=
greenlet
.
greenlet
(
test2
)
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
)
"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
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
:
"catch Exception in test1"
def test2
(
u
)
:
assert
False
gr1
=
greenlet
.
greenlet
(
test1
)
gr2
=
greenlet
.
greenlet
(
test2
)
try
:
gr1
.
switch
(
"hello"
,
" world"
)
except
:
"catch Exception in main"
輸出為:
catch Exception in main
Greenlet生命周期
文章開始的地方提到第一個例子中的gr2其實並沒有正常結束,我們可以借用greenlet.dead這個屬性來查看:
from greenlet import greenlet
def test1
()
:
gr2
.
switch
(
1
)
"test1 finished"
def test2
(
x
)
:
"test2 first"
,
x
z
=
gr1
.
switch
()
"test2 back"
,
z
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
)
gr1
.
switch
()
"gr1 is dead?: %s, gr2 is dead?: %s"
%
(
gr1
.
dead
,
gr2
.
dead
)
gr2
.
switch
()
"gr1 is dead?: %s, gr2 is dead?: %s"
%
(
gr1
.
dead
,
gr2
.
dead
)
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
)
:
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
)
"main id %s, gr1 id %s, gr2 id %s"
%
(
id
(
main
),
id
(
g1
),
id
(
g2
))
oldtrace
=
greenlet
.
settrace
(
callback
)
try
:
g1
.
switch
()
except
:
"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
()
"finish switch del huge"
del
huge
[
:
]
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
)
gr1
.
switch
()
gr1
=
gr2
=
None
"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
:
"finish switch del huge"
del
huge
[
:
]
gr1
=
greenlet
(
test1
)
gr2
=
greenlet
(
test2
)
gr1
.
switch
()
gr1
=
gr2
=
None
"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標準庫:itertools模塊
※矽谷網紅課!19周成為最搶手的深度學習技術人才
※Python vs Ruby: 誰是最好的 web 開發語言?
※學慣用 Python 編程時要避免的 3 個錯誤
TAG:Python開發者 |