1. 程式人生 > >Python與協程從Python2—Python3

Python與協程從Python2—Python3

協程,又稱微執行緒、纖程,英文名Coroutine;用一句話說明什麼是執行緒的話:協程是一種使用者態的輕量級執行緒。

Python對於協程的支援在python2中還比較簡單,但是也有可以使用的第三方庫,在python3中開始全面支援,也成為python3的一個核心功能,很值得學習。

 

協程介紹

協程,又稱微執行緒、纖程,英文名Coroutine;用一句話說明什麼是執行緒的話:協程是一種使用者態的輕量級執行緒。

協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧。因此:協程能保留上一次呼叫時的狀態(即所有區域性狀態的一個特定組合),每次過程重入時,就相當於進入上一次呼叫的狀態,換種說法:進入上一次離開時所處邏輯流的位置。

協程的優點:

1)無需執行緒上下文切換的開銷

2)無需原子操作鎖定及同步的開銷

3)方便切換控制流,簡化程式設計模型

4)高併發+高擴充套件性+低成本:一個CPU支援上萬的協程都不是問題。所以很適合用於高併發處理。

協程的缺點:

1)無法利用多核資源:協程的本質是個單執行緒,它不能同時將 單個CPU 的多個核用上,協程需要和程序配合才能執行在多CPU上

2)進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程式

 

Python2中的協程

yield關鍵字

Python2對於協程的支援,是通過yield關鍵字實現的,下面示例程式碼是一個常見的生產者—消費者模型,程式碼示例如下:


def consumer():

    r = ''

    while True:

        n = yield r

        if not n:

            continue

        print('[CONSUMER] Consuming %s...' % n)

        r = '200 OK'



def produce(c):

    c.next()

    n = 0

    while n < 5:

        n = n + 1

        print('[PRODUCER] Producing %s...' % n)

        r = c.send(n)

        print('[PRODUCER] Consumer return: %s' % r)

    c.close()



if __name__ == '__main__':

    c = consumer()

    produce(c)

執行結果:

注意到consumer函式是一個generator(生成器),把一個consumer傳入produce後:

1)首先呼叫c.next()啟動生成器;

2)然後,一旦生產了東西,通過c.send(n)切換到consumer執行;

3)consumer通過yield拿到訊息,處理,又通過yield把結果傳回;

4)produce拿到consumer處理的結果,繼續生產下一條訊息;

5)produce決定不生產了,通過c.close()關閉consumer,整個過程結束。

整個流程無鎖,由一個執行緒執行,produce和consumer協作完成任務,所以稱為“協程”,而非執行緒的搶佔式多工。

傳統的生產者-消費者模型是一個執行緒寫訊息,一個執行緒取訊息,通過鎖機制控制佇列和等待,但一不小心就可能死鎖。

如果改用協程,生產者生產訊息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高。

Python對協程的支援還非常有限,用在generator中的yield可以一定程度上實現協程。雖然支援不完全,但已經可以發揮相當大的威力了。

 

gevent模組

Python通過yield提供了對協程的基本支援,但是不完全。而第三方的gevent為Python提供了比較完善的協程支援。gevent是第三方庫,通過greenlet實現協程,其基本思想是:

當一個greenlet遇到IO操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在執行,而不是等待IO。由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啟動時通過monkey patch完成。

示例程式碼如下:


from gevent import monkey; monkey.patch_all()

import gevent

import urllib2



def f(url):

    print('GET: %s' % url)

    resp = urllib2.urlopen(url)

    data = resp.read()

    print('%d bytes received from %s.' % (len(data), url))



gevent.joinall([

        gevent.spawn(f, 'https://www.python.org/'),

        gevent.spawn(f, 'https://www.yahoo.com/'),

        gevent.spawn(f, 'https://github.com/'),

])

執行結果:

從執行結果可以看到,網站訪問的順序是自動切換的。

 

gevent優缺

使用gevent,可以獲得極高的併發效能,但gevent只能在Unix/Linux下執行,在Windows下不保證正常安裝和執行。Python創始人Gvanrossum從來不喜歡Gevent,而是更願意另闢蹊徑的實現asyncio(python3中的非同步實現)。

1)Monkey-patching。中文「猴子補丁」,常用於對測試環境做一些hack。Gvanrossum說用它就是”patch-and-pray”,由於Gevent直接修改標準庫裡面大部分的阻塞式系統呼叫,包括socket、ssl、threading和 select等模組,而變為協作式執行。但是無法保證在複雜的生產環境中有哪些地方使用這些標準庫會由於打了補丁而出現奇怪的問題,那麼只能祈禱(pray)了。

2)其次,在Python之禪中明確說過:「Explicit is better than implicit.」,猴子補丁明顯的背離了這個原則。

3)第三方庫支援。得確保專案中用到其他用到的網路庫也必須使用純Python或者明確說明支援Gevent,而且就算有這樣的第三方庫,也需要擔心這個第三方庫的程式碼質量和功能性。

4)Greenlet不支援Jython和IronPython,這樣就無法把gevent設計成一個標準庫了。

之前是沒有選擇,很多人選擇了Gevent,而現在明確的有了更正統的、正確的選擇:asyncio(下一節會介紹)。所以建議大家瞭解Gevent,擁抱asyncio。

另外,如果知道現在以及未來使用Gevent不會給專案造成困擾,那麼用Gevent也是可以的。

 

Python3中的協程

Gvanrossum希望在Python 3 實現一個原生的基於生成器的協程庫,其中直接內建了對非同步IO的支援,這就是asyncio,它在Python 3.4被引入到標準庫。

下面將簡單介紹asyncio的使用:

1)event_loop 事件迴圈:程式開啟一個無限的迴圈,程式設計師會把一些函式註冊到事件迴圈上。當滿足事件發生的時候,呼叫相應的協程函式。

2)coroutine 協程:協程物件,指一個使用async關鍵字定義的函式,它的呼叫不會立即執行函式,而是會返回一個協程物件。協程物件需要註冊到事件迴圈,由事件迴圈呼叫。

3)task 任務:一個協程物件就是一個原生可以掛起的函式,任務則是對協程進一步封裝,其中包含任務的各種狀態。

4)future: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別

5)async/await 關鍵字:python3.5 用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的非同步呼叫介面。

程式碼示例如下:


import asyncio

import time



now = lambda: time.time()

async def do_some_work(x):

    print('Waiting: {}s'.format(x))



    await asyncio.sleep(x)

    return 'Done after {}s'.format(x)



async def main():

    coroutine1 = do_some_work(1)

    coroutine2 = do_some_work(5)

    coroutine3 = do_some_work(3)



    tasks = [

        asyncio.ensure_future(coroutine1),

        asyncio.ensure_future(coroutine2),

        asyncio.ensure_future(coroutine3)

    ]

    done, pending = await asyncio.wait(tasks)

    for task in done:

        print('Task ret: ', task.result())



start = now()



loop = asyncio.get_event_loop()

task = asyncio.ensure_future(main())

try:

    loop.run_until_complete(task)

    print('TIME: ', now() - start)

except KeyboardInterrupt as e:

    print(asyncio.Task.all_tasks())

    print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())

    loop.stop()

    loop.run_forever()

finally:

    loop.close()

執行結果:

可以看到程式執行時間是以等待時間最長的為準。

 

使用async可以定義協程物件,使用await可以針對耗時的操作進行掛起,就像生成器裡的yield一樣,函式讓出控制權。協程遇到await,事件迴圈將會掛起該協程,執行別的協程,直到其他的協程也掛起或者執行完畢,再進行下一個協程的執行。耗時的操作一般是一些IO操作,例如網路請求,檔案讀取等。我們使用asyncio.sleep函式來模擬IO操作。協程的目的也是讓這些IO操作非同步化。

 

Asyncio是python3中一個強大的內建庫,上述只是簡單的介紹了asyncio的用法有興趣的話,很值