Greenlet初識

翻譯自官方文檔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

那么下次編寫并發程序的時候,是不是該考慮一下協程呢?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • 原文:https://greenlet.readthedocs.io/en/latest/ 背景 greenlet...
    林灣村龍貓閱讀 1,230評論 0 4
  • 在之前,我已經在兩篇文章中分別介紹了gevent的使用以及gevent的底層greenlet的使用,可以閱讀文章回...
    WolfLC閱讀 3,344評論 0 7
  • 前述 進程 線程 協程 異步 并發編程(不是并行)目前有四種方式:多進程、多線程、協程和異步。 多進程編程在pyt...
    softlns閱讀 6,336評論 2 24
  • 參考資料 http://www.gevent.org/contents.html https://uwsgi-do...
    JunChow520閱讀 16,927評論 0 10
  • 進程 線程 協程 異步 并發編程(不是并行)目前有四種方式:多進程、多線程、協程和異步。 多進程編程在python...
    hugoren閱讀 4,987評論 1 4