用greenlet實現Python中的并發

dktg4170 7年前發布 | 11K 次閱讀 并發 Python Python開發

在上一篇介紹生成器時,我們講到了協程(Coroutine),它也被稱為微線程。回顧一下,協程可以在一個函數執行過程中將其掛起,去執行另一個函數,并在必要時將之前的函數喚醒。在Python的語言環境里,協程是相當常用的實現“并發”的方法。上一篇的例子中,我們演示了如何使用yield關鍵字來實現協程,不過這個看上去非常不直觀。這里我們要介紹一個非常好用的框架greenlet,很多知名的網絡并發框架如eventlet,gevent都是基于它實現的。

第一個例子

沿襲我們一直以來的習慣,先從例子開始,這次偷個懶,直接把 官方文檔 中的例子拿過來:

from greenletimport 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()

這里創建了兩個greenlet協程對象,gr1和gr2,分別對應于函數test1()和test2()。使用greenlet對象的switch()方法,即可以切換協程。上例中,我們先調用”gr1.switch()”,函數test1()被執行,然后打印出”12″;接著由于”gr2.switch()”被調用,協程切換到函數test2(),打印出”56″;之后”gr1.switch()”又被調用,所以又切換到函數test1()。但注意,由于之前test1()已經執行到第5行,也就是”gr2.switch()”,所以切換回來后會繼續往下執行,也就是打印”34″;現在函數test1()退出,同時程序退出。由于再沒有”gr2.switch()”來切換至函數test2(),所以程序第11行”print 78″不會被執行。

所以,程序運行下來的輸出就是:

很好理解吧。使用switch()方法切換協程,也比”yield”, “next/send”組合要直觀的多。上例中,我們也可以看出,greenlet協程的運行,其本質是串行的,所以它不是真正意義上的并發,因此也無法發揮CPU多核的優勢,不過,這個可以通過協程+進程組合的方式來解決,本文就不展開了。另外要注意的是,在沒有進行顯式切換時,部分代碼是無法被執行到的,比如上例中的”print 78″。

父子關系

創建協程對象的方法其實有兩個參數”greenlet(run=None, parent=None)”。參數”run”就是其要調用的方法,比如上例中的函數test1()和test2();參數”parent”定義了該協程對象的父協程,也就是說,greenlet協程之間是可以有父子關系的。如果不設或設為空,則其父協程就是程序默認的”main”主協程。這個”main”協程不需要用戶創建,它所對應的方法就是主程序,而所有用戶創建的協程都是其子孫。大家可以把greenlet協程集看作一顆樹,樹的根節點就是”main”,上例中的”gr1″和”gr2″就是其兩個字節點。

在子協程執行完畢后,會自動返回父協程。比如上例中test1()函數退出,代碼會返回到主程序。讓我們寫個更清晰的例子來實驗下:

from greenletimport greenlet

def test1():
    print 12
    gr2.switch()
    print 34

def test2():
    print 56

gr1 = greenlet(test1)
gr2 = greenlet(test2, gr1)
gr1.switch()
print 78

這里創建greenlet對象”gr2″時,指定了其父協程是”gr1″。所以在函數test2()里,雖然沒有”gr1.switch()”代碼,但是在其退出后,程序一樣回到了函數test1(),并且執行”print 34″。同樣,在test1()退出后,代碼回到了主程序,并執行”print 78″。所以,最后的輸出就是:

如果上例中,”gr2″的父協程不是”gr1″而是”main”的話,那test2()運行完畢就會回到主程序并直接打印”78″,這樣”print 34″就不會執行。大家可以試一試。

還有一個重要的點,就是協程退出后,就無法再被執行了。如果上例在函數test1()中,再加一句”gr2.switch()”,運行的結果是一樣的。因為第二次調用”gr2.switch()”,什么也不會運行。

def test1():
    print 12
    gr2.switch()
    print 34
    gr2.switch()

大家可能會感覺到父子協程之間的關系,就像函數調用一樣,一個嵌套一個。的確,其實greenlet協程的實現就是使用了棧,其運行的上下文保存在棧中,”main”主協程處于棧底的位置,而當前運行中的協程就在棧頂。這同函數是一樣。此外,在任何時候,你都可以使用”greenlet.getcurrent()”,獲取當前運行中的協程對象。比如在函數test2()中執行”greenlet.getcurrent()”,其返回就等于”gr2″。

異常

既然協程是存放在棧中,那一個協程要拋出異常,就會先拋到其父協程中,如果所有父協程都不捕獲此異常,程序才會退出。我們試下,把上面的例子中函數test2()的代碼改為:

def test2():
    print 56
    raise NameError

程序執行后,我們可以看到Traceback信息:

  File "parent.py", line 14, in 
    gr1.switch()
  File "parent.py", line 5, in test1
    gr2.switch()
  File "parent.py", line 10, in test2
    raiseNameError

同時大家可以試下,如果將”gr2″的父協程設為空,Traceback信息就會變為:

  File "parent.py", line 14, in 
    gr1.switch()
  File "parent.py", line 10, in test2
    raiseNameError

因此,如果”gr2″的父協程是”gr1″的話,異常先回拋到函數test1()的代碼”gr2.switch()”處。所以,我們再對函數test1()改動下:

def test1():
    print 12
    try:
        gr2.switch()
    except NameError:
        print 90
    print 34

運行后的結果,如果”gr2″的父協程是”gr1″,則異常被捕獲,并打印90。否則,異常會被拋出。以上實驗很好的證明了,子協程拋出的異常會根據棧里的順序,依次拋到父協程里。

有一個異常是特例,不會被拋到父協程中,那就是”greenlet.GreenletExit”,這個異常會讓當前協程強制退出。比如,我們將函數test2()改為:

def test2():
    print 56
    raise greenlet.GreenletExit
    print 78

那代碼行”print 78″永遠不會被執行。但這個異常不會往上拋,所以其父協程還是可以正常運行。

另外,我們可以通過greenlet對象的”throw()”方法,手動往一個協程里拋個異常。比如,我們在test1()里調一個throw()方法:

def test1():
    print 12
    gr2.throw(NameError)
    try:
        gr2.switch()
    except NameError:
        print 90
    print 34

這樣,異常就會被拋出,運行后的Trackback是這樣的:

  File "exception.py", line 21, in 
    gr1.switch()
  File "exception.py", line 5, in test1
    gr2.throw(NameError)

如果將”gr2.throw(NameError)”放在”try”語句中,那該異常就會被捕獲,并打印”90″。另外,當”gr2″的父協程不是”gr1″而是”main”時,異常會直接拋到主程序中,此時函數test1()中的”try”語句就不起作用了。

協程間傳遞消息

在介紹生成器時,我們聊過可以使用生成器的send()方法來傳遞參數。greenlet也同樣支持,只要在其switch()方法調用時,傳入參數即可。我們再來基于本文第一個例子改造下:

from greenletimport greenlet

def test1():
    print 12
    y = gr2.switch(56)
    print y

def test2(x):
    print x
    gr1.switch(34)
    print 78

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

在test1()中調用”gr2.switch()”,由于協程”gr2″之前未被啟動,所以傳入的參數”56″會被賦在test2()函數的參數”x”上;在test2()中調用”gr1.switch()”,由于協程”gr1″之前已執行到第5行”y = gr2.switch(56)”這里,所以傳入的參數”34″會作為”gr2.switch(56)”的返回值,賦給變量”y”。這樣,兩個協程之間的互傳消息就實現了。

讓我們將上一篇介紹生成器時寫的生產者消費者的例子,改為greenlet實現吧:

from greenletimport greenlet

def consumer():
    last = ''
    while True:
        receival = pro.switch(last)
        if receivalis not None:
            print 'Consume %s' % receival
            last = receival

def producer(n):
    con.switch()
    x = 0
    while x < n:
        x += 1
        print 'Produce %s' % x
        last = con.switch(x)

pro = greenlet(producer)
con = greenlet(consumer)
pro.switch(5)

 

 

來自:http://python.jobbole.com/87182/

 

 本文由用戶 dktg4170 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!