原文:https://greenlet.readthedocs.io/en/latest/
博客地址:http://www.jianshu.com/u/5a327aab786a
背景
greenlet
包是Stackless的衍生產品,它是一個支持微線程(叫tasklets)的CPython版本。Tasklets運行在僞併發模式下(通常在一個或少許的OS級別的線程),他們通過“channels”來交互數據。
另一方面來說, 一個“greenlet”任然是一個沒有內部調度的關於微線程的較爲原始的概念。換句話說,當你想要在你代碼運行時做到準確控制,“greenlet”是一種很有用的方式。在greenlet基礎之上,你可以定義自己的微線程調度策略。不管怎樣,greenlets也可以以一種高級控制流結構的方式用於他們自己。舉個例子,我們可以重新生成迭代器。python自帶的生成器與greenlet的生成器之間的區別是greenlet的生成器可以嵌套調用函數,並且嵌套函數也會yield值(補充說明的是,你不需要使用yield關鍵詞,參見例子:test_generator.py)。
例子
我們來考慮一個用戶輸入命令的終端控制檯系統。假設輸入是逐個字符輸入。在這樣的一個系統中,有個典型的循環如下所示:
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 # ignore the command
process_command(line)
現在,假設你將程序移植到GUI程序中,絕大部分的GUI成套工具是基於事件驅動的。他們爲每一個用戶字符輸入調用一個回調函數。(將“GUI”替換成“XML expat parser”,對你來說應該更加熟悉了)。在這樣的情形中,執行下面的函數read_next_char()是很困難的。這裏是兩個不兼容的函數:
def event_keydown(key):
??
def read_next_char():
?? should wait for the next event_keydown() call
你可能考慮用線程的方式來實現這個了。greenlets是另一種不需要關聯鎖與沒有當機問題的可選的解決方案。你執行process_commands(),獨立的greenlet。通過如下方式輸入字符串。
def event_keydown(key):
# jump into g_processor, sending it the key
g_processor.switch(key)
def read_next_char():
# g_self is g_processor in this simple example
g_self = greenlet.getcurrent()
# jump to the parent (main) greenlet, waiting for the next key
next_char = g_self.parent.switch()
return next_char
g_processor = greenlet(process_commands)
g_processor.switch(*args) # input arguments to process_commands()
gui.mainloop()
這個例子中,執行流程如下:
- 當作爲g_processor greenlet一部分的read_next_char()函數被調用,所以當接收到輸入切換到上級greenlet, 程序恢復到主循環(GUI)執行。
- 當GUI調用event_keydown()的時候,程序切換到g_processor。這就意味着程序跳出,無論它被掛起在這個greenlet什麼地方。在這個例子中,切換到read_next_char(),並且在event_keydown()中被按下的key作爲switch()的結果返回給了read_next_char()。
需要說明的是read_next_char()的掛起與恢復都保留其調用堆棧。以便在prprocess_commands()中根據他來的地方恢復到不同的位置。這使得以一種好的控制流來控制程序邏輯成爲可能。我們不必完整的重寫process_commands(),將其轉換爲狀態機。
用法
序言
“greenlet” 是微型的獨立的僞線程。考慮到作爲一個幀堆棧。最遠的幀(最底層)是你調用的最初的函數,最外面的幀(最頂層)是在當前greenlet被壓進去的。當你使用greenlets的時候是通過創建一系列的這種堆棧,然後在他們之間跳轉執行。這種跳轉將會導致先前的幀掛起,最後的幀從掛起狀態恢復。在greenlets之間的跳轉關係叫做“switching(切換)”。
當你創建一個greenlet,它將有一個初始化的空堆棧。當你第一次切換到它,它開始運行一個具體的函數。在這個函數中可能調用其他的函數,從當前greenlet中切換出去,等等。當最底層的函數完成執行,greenlet的棧再次爲空,這時,greenlet死亡。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()
- 最後一行跳轉到test1, 然後打印12,
- 跳轉到test2, 然後打印56
- 跳轉回test1, 打印34, test1完成,並且gr1死亡。與此同時,程序執行返回到
gr1.switch()
調用。 - 需要說明的是78從來都沒有打印。
父級greenlet
讓我們看看當greenlet死亡的時候,程序執行到哪裏去了。每一個greenlet都有一個父級greenlet。最初的父級是創建greenlet的那一個greenlet(父級greenlet是可以在任何時候被改變)。父級greenlet是當一個greenlet死亡的時候程序繼續執行的地方。這種方式,程序組織成一顆樹。不在用戶創建的greenlet中運行的頂層代碼在隱式的主greenlet中運行,它是堆棧數的根。
在上面的例子中,gr1與gr2將主greenlet作爲父級greenlet。無論它們中的誰執行完畢,程序執行都會返回到”main”greenlet中。
沒有捕獲的異常將拋出到父級greenlet中。舉個例子,如果上面的test2()包含一個語法錯誤,它將生成一個殺死gr2的NameError錯誤,這個錯誤將直接跳轉到主greenlet。錯誤堆棧將顯示test2,而不會是test1。需要注意的是,switches不是調用,而是程序在並行的”stack container(堆棧容器)”直接執行的跳轉,“parent”定義了邏輯上位於當前greenlet之下的堆棧。
實例化對象
greenlet.greenlet
是一個協程類型,它支持一下操作:
greenlet(run=None,parent=None)
:創建一個新的greenlet對象(還沒有開始運行)。run
是一個可調用的函數,用來被調用。parent
定義父級greenlet,默認是當前greenlet。greenlet.getcurrent()
:獲取當前greenlet(即,調用該函數的greenlet)greenlet.GreenletExit
:這個特殊的異常不會拋出到父級greenlet中,這可以用來殺死一個單一的greenlet。
greenlet
類型可以被子類化。通過調用在greenlet創建的時候初始化的run
屬性來執行一個greenlet。但是對於子類來說,定義一個run
方法比提供一個run
參數給構造器更有意義。
切換
當在一個greenlet中調用方法switch(),在greenlet之間的切換將發生,正常情況下,程序執行跳轉到switch()被調用的greenlet中。或者當一個greenlet死亡,程序執行將跳轉到父級greenlet程序中,當發生切換的時候,一個對象或一個異常被髮送到目標greenlet中。這是一種在兩個greenlet中傳遞信息的便利的方式。舉個例子:
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()的參數不是在greenlet創建的時候給的,而是在第一次切換的時候給出。
這裏給出了關於發送的數據的明確的規則:
g.switch(*args, **kwargs)
:切換執行到greenlet g
,發送數據,作爲一個特殊的例子,如果g
沒有執行,它將開始執行。
對於將死的greenlet。當run()完成的時候,將會發生對象給父級greenlet。如果greenlet因爲異常而終止,這個異常將會拋出到父級greenlet中(greenlet.GreenletExit例外,這個異常被捕獲了並且直接退出到父級greenlet中)。
除了上面例子描述的,通常目標greenlet(父級greenlet)接收之前調用switch()掛起,執行完畢返回的返回值作爲結果。事實上,雖然對switch()的調用不會立即返回結果,但是當其他一些greenlet切換回來的時候,在將來的某個點將會返回結果。當切換髮生的時候,程序將在它之前掛起的地方恢復。switch()自己返回發生的對象。這就意味着x=g.switch(y)
將y
給g
,稍後將返回從某個不關聯的greenlet中返回的不關聯的對象給x
變量。
提醒一下,任何試圖切換到一個死亡的greenlet的將會走到死亡greenlet的父級,或者父級的父級,以此類推(最終的父級是“main” greenlet,它是從來不會死掉的)。
greenlets的方法與屬性
g.switch(*args, **kwargs)
:切換程序到greenletg
中執行,參見上面。g.run
:當它開始的時候,g
的回調將會被執行,當g
已經開始執行了,這個屬性將不會存在了。g.parent
:父級greenlet。這是可編輯屬性,但是不能夠寫成了死循環。g.gr_frame
:最頂層的結構,或者等於None。g.dead
: bool值,當g
死亡了,值爲True。bool(g)
:bool值,當返回結構是True,表示g
還活躍,如果是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
異常,這個異常將不會從g_raiser
傳播到g
。
Greenlets與python的線程
Greenlets將可以和python線程結合起來。這種情況下,每一個線程包含一個獨立的帶有一個子greenlets樹的“main” greenlet。混合或切換在不同線程中的greenlets是不可能的事情。
greenlets的垃圾回收生命週期
如果對一個greenlet的所有關聯都已經失效(包括來自其他greenlets中的父級屬性的關聯),這時候,沒有任何一種方式切換回該greenlet中。這種情況下,GreenletExit異常將會產生。這是一個greenlet接受異步執行的唯一方式。使用try:finally:
語句塊來清理被這個greenlet使用的資源。這種屬性支持一種編程風格,greenlet無限循環等待數據並且執行。當對該greenlet的最後關聯失效,這種循環將自動終止。
如果greenlet要麼死亡,要麼根據存在某個地方的關聯恢復。只需要捕獲與忽略可能導致無限循環的GreenletExit。
Greenlets不參與垃圾回收。循環那些在greenlet框架中的數據時候,這些數據將不會被檢測到。循環的存儲其他greenlets的引用將可能導致內存泄漏。
錯誤堆棧支持
當使用greenlet的時候,標準的python錯誤堆棧與描述將不會按照預期的運行,因爲堆棧與框架的切換髮生在相同的線程中。使用傳統的方法可靠的檢測greenlet切換是一件很困難的事情。因此,爲了改善對greenlet基礎代碼的調試,錯誤堆棧,問題描述的支持,在greenlet模塊中,有一些新的方法:
greenlet.gettrace()
:返回先前已有的調用堆棧方法,或者None。greenlet.settrace(callback)
:設置一個新的調用堆棧方法,返回前期已有的方法或者None。當某些事件發生時,這個回調函數被調用,可以永安裏做一下信號處理。def callback(event, args): if event == 'switch': origin, target = args # Handle a switch from origin to target. # Note that callback is running in the context of target # greenlet and any exceptions will be passed as if # target.throw() was used instead of a switch. return if event == 'throw': origin, target = args # Handle a throw from origin to target. # Note that callback is running in the context of target # greenlet and any exceptions will replace the original, as # if target.throw() was used with the replacing exception. return
爲了兼容,當事件要麼是
switch
要麼是throw
,而不是其他可能的事件時候,將參數解包成tuple。這樣,API可能擴展出於sys.settrace()
相似的新的事件。
C API 相關
Greenlets可以通過用C/C++寫的擴展模塊來生成與維護,或者來自於嵌入到python中的應用。greenlet.h
頭文件被提供,用來展示對原生的python模塊的完整的API訪問。
類型
Type name | Python name |
---|---|
PyGreenlet | greenlet.greenlet |
異常
Type name | Python name |
---|---|
PyExc_GreenletError | greenlet.error |
PyExc_GreenletExit | greenlet.GreenletExit |
關聯
PyGreenlet_Import()
:一個宏定義,導入greenlet模塊,初始化C API。必須在每一個用到greenlet C API的模塊中調用一次。int PyGreenlet_Check(PyObject *p)
:一個宏定義,如果參數是PyGreenlet返回true。int PyGreenlet_STARTED(PyGreenlet *g)
:一個宏定義,如果greenlet在開始了返回true。int PyGreenlet_ACTIVE(PyGreenlet *g)
:一個宏定義,如果greenlet在活動中返回true。PyGreenlet *PyGreenlet_GET_PARENT(PyGreenlet *g)
:一個宏定義,返回greenlet中的父級greenlet。int PyGreenlet_SetParent(PyGreenlet *g, PyGreenlet *nparent)
:設置父級greenlet。返回0爲設置成功,-1,表示g
不是一有效的PyGreenlet指針,AttributeError將拋出。PyGreenlet *PyGreenlet_GetCurrent(void)
:返回當前活躍的greenlet對象。PyGreenlet *PyGreenlet_New(PyObject *run, PyObject *parent)
:使用run
與parent
創建一個新的greenlet對象。這兩個參數是可選的。如果run
是NULL。這個greenlet創建,如果切換開始將失敗。如果parent是NULL。這個parent將自動設置成當前greenlet。PyObject *PyGreenlet_Switch(PyGreenlet *g, PyObject *args, PyObject *kwargs)
:切換到greenetg
。args
與kwargs
是可選的,可以爲NULL。如果args
爲NULL,一個空的tuple將發送給目標greenletg
。如果kwargs
是NULL的。沒有key-value參數發送。如果指定參數,那麼args
應該是一個tuple,kwargs
應該是一個dict。PyObject *PyGreenlet_Throw(PyGreenlet *g, PyObject *typ, PyObject *val, PyObject *tb)
:切換到greenletg
,並且立馬拋出typ
參數(攜帶的值val
)指定的異常,調用堆棧對象tb
是可選的,並且可以爲NULL。