[譯文]greenlet:輕量級併發程式

林灣村龍貓發表於2019-02-02

原文:greenlet.readthedocs.io/en/latest/

部落格地址:www.jianshu.com/u/5a327aab7…

背景

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(`
`):
            line += read_next_char()
        if line == `quit
`:
            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)yg,稍後將返回從某個不關聯的greenlet中返回的不關聯的物件給x變數。

提醒一下,任何試圖切換到一個死亡的greenlet的將會走到死亡greenlet的父級,或者父級的父級,以此類推(最終的父級是“main” greenlet,它是從來不會死掉的)。

greenlets的方法與屬性

  • g.switch(*args, **kwargs):切換程式到greenlet g中執行,參見上面。
  • 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):使用runparent建立一個新的greenlet物件。這兩個引數是可選的。如果run是NULL。這個greenlet建立,如果切換開始將失敗。如果parent是NULL。這個parent將自動設定成當前greenlet。
  • PyObject *PyGreenlet_Switch(PyGreenlet *g, PyObject *args, PyObject *kwargs):切換到greenet gargskwargs是可選的,可以為NULL。如果args為NULL,一個空的tuple將傳送給目標greenlet g。如果kwargs是NULL的。沒有key-value引數傳送。如果指定引數,那麼args應該是一個tuple,kwargs應該是一個dict。
  • PyObject *PyGreenlet_Throw(PyGreenlet *g, PyObject *typ, PyObject *val, PyObject *tb):切換到greenlet g,並且立馬丟擲typ引數(攜帶的值val)指定的異常,呼叫堆疊物件tb是可選的,並且可以為NULL。

索引與表

相關文章