翻譯自官方文檔greenlet。
什么是greenlet
greenlet是從Stackless中分離的項目。greenlet也叫微線程、協程,它的調度是由程序明確控制的,所以執行流程是固定的、明確的。而線程的調度完全交由操作系統,執行順序無法預料。同時,協程之間切換的代價遠比線程小。
greenlet是通過C擴展實現的。
示例
有這么一個系統,它根據用戶在終端輸入命令的不同而執行不同的操作,假設輸入是逐字符的。部分代碼可能是這樣的:
def process_commands(*args):
while True:
line = ''
while not line.endswith('\n'):
line += read_next_char()
if line == 'quit\n':
print "are you sure?"
if read_next_char() != 'y':
continue # 忽略當前的quit命令
process_command(line)
現在我們想把這個程序在GUI中實現。然而大多數GUI庫都是事件驅動的,每當用戶輸入都會調用一個回調函數去處理。在這種情況下,如果還想用上面的代碼邏輯,可能是這樣的:
def event_keydown(key):
??
def read_next_char():
?? # 必須等待下一個event_keydown調用
read_next_char
要阻塞等待event_keydown
調用,然后就會和事件循環相沖突。這種需要并發的情況是可以用多線程來處理,但是我們有更好的方法,就是greenlet。
def event_keydown(key):
# 跳到g_processor,將key發送過去
g_processor.switch(key)
def read_next_char():
# 在這個例子中,g_self就是g_processor
g_self = greenlet.getcurrent()
# 跳到父greenlet,等待下一個Key
next_char = g_self.parent.switch()
return next_char
g_processor = greenlet(process_commands)
g_processor.switch(*args)
gui.mainloop()
我們先用process_commands
創建一個協程,然后調用switch切換到process_commands
中去執行,并輸入參數args。在process_commands
中運行到read_next_char
,又切換到主協程,執行gui.mainloop()
,在事件循環中等待鍵盤按下的動作。當按下某個鍵之后,調用event_keydown
,切換到g_processor
,并將key傳過去。read_next_char
恢復運行,接收到key,然后返回給process_commands
,處理完之后又暫停在read_next_char
等待下一次按鍵。
下面我們來詳細講解greenlet的用法。
用法
簡介
一個協程是一個獨立的“假線程”。可以把它想成一個小的幀棧,棧底是你調用的初始函數,棧頂是greenlet當前暫停的地方。我們使用協程,實際上就是創建了一系列這樣幀棧,然后在它們之間跳轉執行。而跳轉必須是明確的,跳轉也稱為'switching'。
當你創建一個協程時,產生一個空的棧,在第一次切換到這個協程時,它調用一個特殊的函數,這個函數中可以調用其他函數,可以切換到其他協程等等。當最終棧底函數執行完后,協程的棧變為空,這時候,協程是死的(dead)。協程也可能由于異常而死亡。
下面是個非常簡單的例子:
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()
最后一行切換到test1,打印12,切換到test2,打印56,又切回到test1打印34。然后test1結束,gr1死亡。這時候執行回到了gr1.switch()
調用。注意到,78并沒有被打印出。
父協程
每個協程都有一個父協程。協程在哪個協程中被創建,那么這個協程就是父協程,當然后面可以更改。當某個協程死亡后,會在父協程中繼續執行。舉個例子,在g1中創建了g2,那么g1就是g2的父協程,g2死亡后,會在g1中繼續執行。這么說的話,協程是樹結構的。最上層的代碼不是運行在用戶定義的協程中,而是在一個隱式的主協程中,它是協程樹的根(root)。
在上面的例子中,gr1和gr2的父協程都是主協程。不管哪一個死亡,執行都會回到主協程。
異常也會被傳到父協程。比如說,test2中若包含了一個'typo',就會引發NameError異常,然后殺死gr2,執行會直接回到主協程。Traceback會顯示test2而不是test1。注意,協程的切換不是調用,而是在平行的"棧容器"中傳遞執行。
協程類
greenlet.greenlet就是協程類,它支持下面一些操作:
greenlet(run=None, parent=None)
創建一個新的協程對象。run是一個可調用對象,parent是父協程,默認是當前協程。
greenlet.getcurrent()
返回當前協程,也就是調用這個函數的協程。
greenlet.GreenletExit
這個特殊的異常不會傳給父協程,常用來殺死協程。
greenlet是可以被繼承的。協程通過執行run屬性來運行。在子類中,可以自由地去定義run,而不是一定要傳遞run參數給構造器。
切換
有兩種情況會發生協程之間的切換。一是某個協程主動調用switch方法,這種情況下會切換到被調用的協程中。二是協程死亡,這時協程會切換到父協程。在切換時,一個對象或異常被傳遞到目標協程。這用來在協程之間傳遞信息。如下面這個例子:
def test1(x, y):
z = gr2.switch(x+y)
print z
def test2(u):
print u
gr1.switch(42)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", " world")
這個代碼會打印"hello world"和42。注意到,test1和test2在協程創建時并沒有提供參數,而是在第一次切換的地方。
g.switch(*args, **kwargs)
切換到協程g執行,傳遞提供的參數。如果g還沒運行,那么傳遞參數給g的run屬性,并開始執行run()。
如果協程的run()執行結束,return的值會返回給主協程。如果run()以異常方式結束,異常會傳遞給主協程(除非是greenlet.GreenletExit
,這種情況下會直接返回到主協程)。
如果切換到一個已死亡的的協程,那么實際上是切換到它的父協程,依次類推。
協程的方法和屬性
g.switch(*args, **kwargs)
切換到協程g執行,見上面。
g.run
一個可調用對象,當g開始執行時,調用它。但是一旦開始執行后,這個屬性就不存在了。
g.parent
父協程,這個值是可以改變的,但是不允許創建循環的父進程。
g.gr_frame
當前最頂層的幀,或者是None。
g.dead
如果協程已死亡,那么值是True。
bool(g)
如果協程處于活躍狀態,則為True。如果已死亡或者未開始執行則為False。
g.throw([typ, [val, [tb]]])
切換到g執行,但是立刻引發異常。如果沒有參數,則默認引發greenlet.GreenletExit異常。這個方法的執行類似于:
def raiser():
raise typ, val, tb
g_raiser = greenlet(raiser, parent=g)
g_raiser.switch()
當然greenlet.GreenletExit除外。
協程和Python線程
協程可以和線程組合使用。每個線程包含一個獨立的主協程和協程樹。當然不同線程的協程之間是無法切換執行的。
垃圾收集
如果對一個協程的引用計數為0,那么就沒辦法再次切換到這個協程。這種情況下,協程會產生一個GreenletExit異常。這是協程唯一一種異步接收到GreenletExit異常的情況。可以用try...finally...來清除協程的資源。這個特性允許我們用無限循環的方式來等待數據并處理,因為當協程的引用計數變成0時,循環會自動中斷。
在無限循環中,如果想要協程死亡就捕獲GreenletExit異常。如果想擁有一個新的引用就忽略GreenletExit。
greenlet不參與垃圾收集,目前協程幀的循環引用數據不會被檢測到。循環地將引用存到其他協程會導致內存泄漏。
追蹤支持
當我們使用協程的時候,標準的Python追蹤和性能分析無能為力,因為協程的切換時在單個線程中。很難通過簡單的方法來偵測到協程的切換,所以為了提高對調試的支持,增加了下面幾個新的函數:
greenlet.gettrace()
返回之前的追蹤函數設置,或者None。
greenlet.settrace(callback)
設置一個新的追蹤函數,返回之前的,或者None。這個函數類似于sys.settrace()
各種事件發生的時候都會調用callback,并且callback是下面這樣的:
def callback(event, args):
if event == 'switch':
origin, target = args
# 處理從origin到target的切換
# 注意callback在target的上下文中執行
return
if event == 'throw':
origin, target = args
# 處理從origin到target的拋出
# 注意callback在target的上下文中執行
return
那么下次編寫并發程序的時候,是不是該考慮一下協程呢?