Python與協程從Python2—Python3

協程,又稱微線程、纖程,英文名Coroutine;用一句話說明什麼是線程的話:協程是一種用戶態的輕量級線程。python

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

 

協程介紹

協程,又稱微線程、纖程,英文名Coroutine;用一句話說明什麼是線程的話:協程是一種用戶態的輕量級線程。程序員

協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。所以:協程能保留上一次調用時的狀態(即全部局部狀態的一個特定組合),每次過程重入時,就至關於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。github

協程的優勢:編程

1)無需線程上下文切換的開銷網絡

2)無需原子操做鎖定及同步的開銷併發

3)方便切換控制流,簡化編程模型異步

4)高併發+高擴展性+低成本:一個CPU支持上萬的協程都不是問題。因此很適合用於高併發處理。socket

協程的缺點:async

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的用法有興趣的話,很值得去學習一下!

相關文章
相關標籤/搜索