greenlet 詳解

   在前面的文章中提到python原生的generator是semicoroutine,而greenlet是 真 協程。本文內容主要來自對官網文檔的翻譯,在其中也加入了不少本身的理解和例子。主要包括如下內容:什麼是greenlet,greenlet的切換與函數調用的區別,greenlet的生命週期,以及使用greenlet的注意事項。html

greenlet初體驗

  Greenlet是python的一個C擴展,來源於Stackless python,旨在提供可自行調度的‘微線程’, 即協程。generator實現的協程在yield value時只能將value返回給調用者(caller)。 而在greenlet中,target.switch(value)能夠切換到指定的協程(target), 而後yield value。greenlet用switch來表示協程的切換,從一個協程切換到另外一個協程須要顯式指定。python

  greenlet的安裝很簡單:pip install greenlet 便可,安裝好了以後咱們來看一個官方的例子程序員

 1 from greenlet import greenlet
 2 def test1():
 3     print 12
 4     gr2.switch()
 5     print 34
 6 
 7 def test2():
 8     print 56
 9     gr1.switch()
10     print 78
11 
12 gr1 = greenlet(test1)
13 gr2 = greenlet(test2)
14 gr1.switch()

  輸出爲:
    12 56 34less

  當建立一個greenlet時,首先初始化一個空的棧, switch到這個棧的時候,會運行在greenlet構造時傳入的函數(首先在test1中打印 12), 若是在這個函數(test1)中switch到其餘協程(到了test2 打印34),那麼該協程會被掛起,等到切換回來(在test2中切換回來 打印34)。當這個協程對應函數執行完畢,那麼這個協程就變成dead狀態。
  ssh

  注意 上面沒有打印test2的最後一行輸出 78,由於在test2中切換到gr1以後掛起,可是沒有地方再切換回來。這個可能形成泄漏,後面細說。異步

greenlet module與class

  咱們首先看一下greenlet這個module裏面的屬性函數

  >>> dir(greenlet)
  ['GREENLET_USE_GC', 'GREENLET_USE_TRACING', 'GreenletExit', '_C_API', '__doc__', '__file__', '__name__', '__package__', '__version__', 'error', 'getcurrent', 'gettrace', 'greenlet', 'settrace']
  >>>學習

  其中,比較重要的是getcurrent(), 類greenlet、異常類GreenletExit。spa

  getcurrent()返回當前的greenlet實例線程

  GreenletExit:是一個特殊的異常,當觸發了這個異常的時候,即便不處理,也不會拋到其parent(後面會提到協程中對返回值或者異常的處理)

  而後咱們再來看看greenlet.greenlet這個類:

  >>> dir(greenlet.greenlet)

  ['GreenletExit', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getstate__', '__hash__', '__init__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',   '__sizeof__', '__str__', '__subclasshook__', '_stack_saved', 'dead', 'error', 'getcurrent', 'gettrace', 'gr_frame', 'parent', 'run', 'settrace','switch', 'throw']
  >>> 

  比較重要的幾個屬性:

  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。

 1 import greenlet
 2 def test1(x, y):
 3     z = gr2.switch(x+y)
 4     print('test1 ', z)
 5 
 6 def test2(u):
 7     print('test2 ', u)
 8     gr1.switch(10)
 9 
10 gr1 = greenlet.greenlet(test1)
11 gr2 = greenlet.greenlet(test2)
12 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」。

 1 import greenlet
 2 def test1(x, y):
 3     print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240272 40239952
 4     z = gr2.switch(x+y)
 5     print 'back z', z
 6 
 7 def test2(u):
 8     print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240352 40239952
 9     return 'hehe'
10 
11 gr1 = greenlet.greenlet(test1)
12 gr2 = greenlet.greenlet(test2)
13 print id(greenlet.getcurrent()), id(gr1), id(gr2)     # 40239952, 40240272, 40240352
14 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

mport 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這個屬性來查看

 1 from greenlet import greenlet
 2 def test1():
 3     gr2.switch(1)
 4     print 'test1 finished'
 5 
 6 def test2(x):
 7     print 'test2 first', x
 8     z = gr1.switch()
 9     print 'test2 back', z
10 
11 gr1 = greenlet(test1)
12 gr2 = greenlet(test2)
13 gr1.switch()
14 print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
15 gr2.switch()
16 print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
17 print gr2.switch(10)

  輸出:

  test2 first 1
  test1 finished
  gr1 is dead?: True, gr2 is dead?: False
  test2 back ()
  gr1 is dead?: True, gr2 is dead?: True
  10

  從這個例子能夠看出

  • 只有當協程對應的函數執行完畢,協程纔會die,因此第一次Check的時候gr2並無die,由於第9行切換出去了就沒切回來。在main中再switch到gr2的時候, 執行後面的邏輯,gr2 die
  • 若是試圖再次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()

輸出:

  main id 40604416, gr1 id 40604736, gr2 id 40604816
  switch from 40604416 to 40604736
  switch from 40604736 to 40604816
  throw from 40604816 to 40604416
  Exception

  其中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. 「

  對於第一點,咱們來看一個例子:  

 1 from greenlet import greenlet, GreenletExit
 2 huge = []
 3 def show_leak():
 4     def test1():
 5         gr2.switch()
 6 
 7     def test2():
 8         huge.extend([x* x for x in range(100)])
 9         gr1.switch()
10         print 'finish switch del huge'
11         del huge[:]
12     
13     gr1 = greenlet(test1)
14     gr2 = greenlet(test2)
15     gr1.switch()
16     gr1 = gr2 = None
17     print 'length of huge is zero ? %s' % len(huge)
18 
19 if __name__ == '__main__':
20     show_leak() 
21    # output: length of huge is zero ? 100

  在test2函數中 第11行,咱們將huge清空,而後再第16行將gr一、gr2的引用計數降到了0。但運行結果告訴咱們,第11行並無執行,因此若是一個協程沒有正常結束是很危險的,每每不符合程序員的預期。greenlet提供瞭解決這個問題的辦法,官網文檔提到:若是一個greenlet實例的引用計數變成0,那麼會在上次掛起的地方拋出GreenletExit異常,這就使得咱們能夠經過try ... finally 處理資源泄露的狀況。以下面的代碼:

  

 1 from greenlet import greenlet, GreenletExit
 2 huge = []
 3 def show_leak():
 4     def test1():
 5         gr2.switch()
 6 
 7     def test2():
 8         huge.extend([x* x for x in range(100)])
 9         try:
10             gr1.switch()
11         finally:
12             print 'finish switch del huge'
13             del huge[:]
14     
15     gr1 = greenlet(test1)
16     gr2 = greenlet(test2)
17     gr1.switch()
18     gr1 = gr2 = None
19     print 'length of huge is zero ? %s' % len(huge)
20 
21 if __name__ == '__main__':
22     show_leak()
23     # output :
24     # finish switch del huge
25    # 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也不會出異常。

  看上去貌似解決了問題,但這對程序員要求過高了,百密一疏。因此最好的辦法仍是保證協程的正常結束。

總結:

  以前的文章其實已經提到提到了coroutine協程的強大之處,對於異步非阻塞,並且還須要保留上下文的場景很是適用。greenlet跟強大,能夠從一個協程切換到任意其餘協程,這是generator作不到的,但這種能力其實也是雙刃劍,前面的注意事項也提到了,必須保證greenlet的正常結束,在協程之間任意的切換很容易出問題。

  好比對於服務之間異步請求的例子,簡化爲服務A的一個函數foo須要異步訪問服務B,能夠這樣封裝greenlet:用decorator裝飾函數foo,當調用這個foo的時候創建一個greenlet實例,併爲這個greenley對應一個惟一的gid,在foo方法發出異步請求(寫到gid)以後,switch到parent,這個時候這個新的協程處於掛起狀態。當請求返回以後,經過gid找到以前被掛起的協程,恢復該協程便可。More simple More safety,保證旨在main和一級子協程之間切換。須要注意的是處理各類異常 以及請求超時的狀況,避免內存泄露,gvent對greenlet的使用大體也是這樣的。

 

references:

http://www.cnblogs.com/xybaby/p/6323358.html
https://pypi.python.org/pypi/greenlet

https://en.wikipedia.org/wiki/Stackless_Pythonhttp://greenlet.readthedocs.io/en/latest/http://stackoverflow.com/questions/715758/coroutine-vs-continuation-vs-generator

相關文章
相關標籤/搜索