在python之gevent(1)一文中我們簡單的介紹了gevent的使用。python由于GIL的原因,導致線程性能嚴重下降,實際可以認為是偽線程,無法達到我們在使用線程時候的預期。而gevent就是一個現在很火、支持也很全面的python第三方協程庫,可以讓python代碼很方便的使用線程。在更深入的學習gevent的源碼前,我們先一起學習了解一下gevent實現的基礎——greenlet。
Greenlet是python的一個C擴展,旨在提供可自行調度的‘微線程’, 即協程。generator實現的協程在yield value時只能將value返回給調用者(caller)。 而在greenlet中,target.switch(value)可以切換到指定的協程(target), 然后yield value。greenlet用switch來表示協程的切換,從一個協程切換到另一個協程需要顯式指定。
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狀態。
greenlet的module與class
一起看一下greenlet中的屬性:
其中,比較重要的是getcurrent(), 類greenlet、異常類GreenletExit。
getcurrent()返回當前的greenlet實例;
GreenletExit:是一個特殊的異常,當觸發了這個異常的時候,即使不處理,也不會拋到其parent(后面會提到協程中對返回值或者異常的處理)
然后我們再來看看greenlet.greenlet這個類:
比較重要的幾個屬性:
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)
輸出如下:
從這個例子可以看出:
1.只有當協程對應的函數執行完畢,協程才會die,所以第一次Check的時候gr2并沒有die,因為第9行切換出去了就沒切回來。在main中再switch到gr2的時候, 執行后面的邏輯,gr2 die
2.如果試圖再次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()
其中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也不會出異常。
看上去貌似解決了問題,但這對程序員要求太高了,百密一疏。所以最好的辦法還是保證協程的正常結束。
以上便是greenlet的基本使用,下一次我們將在此基礎上繼續進行gevent的學習。
喜歡的朋友歡迎點個贊再走哈哈。