python實現協程(一)

        python生成器中用到的 yield item 具有2個含義“產出”和“讓步”。yield item這行代碼會產出一個值,提供給next()調用方;此外還會做出讓步,即暫停執行生成器,讓調用方繼續工作,直到需要使用另一個值時,纔會回到生成器上次退出的地方繼續執行。

        從句法上看,協程與生成器類似,都是包含yield關鍵字的函數。但在協程中,yield表達式通常爲:data = yield,可以產出值,也可以不產出(如果yield關鍵字後面沒有表達式,那麼生成器產出None)。此外,協程通常會從調用方接收數據,調用方把數據提供給協程使用.send(data)方法。

        yield關鍵字甚至還可以不接收或傳出數據。不管數據如何流動,yield都是一種流程控制工具,使用它可以實現協作式多任務:協程可以把控制器讓步給中心調度程序,從而激活其它的協程。

一. 協程的概念

       協程(Coroutine),又稱微線程,纖程,但協程本質上是一個線程在運行。線程比進程輕量,而協程比線程還要輕量;多線程在同一個進程中運行,而協程通常也在在同一個線程中運行。

      由於CPython解析器的GIL原因,多線程的效率受到了很大制約,並且在類*inux系統中,創建線程的開銷並不比進程小。後來人們發現通過yield來中斷代碼片段的執行, 同時交出了CPU的使用權,於是線程的概念產生了,並在python3.4中正式引入。協程通過應用程序,記錄上下文棧區,實現在程序執行過程中的跳躍執行。由此可以選擇不阻塞的部分執行以提升運行效率。和多線程相比,協程具有如下優點:

  • 線程是系統級別的它們由操作系統調度,而協程則是程序級別的由程序根據需要自己調度;
  • 資源消耗少,無需多線程那樣進行多核間的切換;
  • 無需同步互斥操作;
  • 沒有C10K問題,IO併發性好,一個CPU支持上萬的協程都不是問題,所以很適合用於高併發處理。

      協程的缺點:

  • 無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上。當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。

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

二. 使用yield實現協程

        本部分使用到的主要方法總結如下:

生成器API新增的方法/特性 說明
send(value) 調用方可以使用.send()方法發送數據,發送的數據會成爲生成器函數中yield表達式的值。
throw(...) 調用方拋出異常,並在生成器中處理。
close() 終止生成器。
return 生成器可以使用return返回一個值。
yield from 把複雜的生成器重構成小型的嵌套生成器,省去了之前把生成器工作委託給子生成器所需的大量樣板代碼。

2.1 用作協程的生成器的基本行爲

 使用生成器函數定義協程的例子:

def simple_coroutine():
    print('-> coroutine started')
    x = yield
    print('-> coroutine received:', x)


coro = simple_coroutine()
next(coro)
coro.send('this message from caller.')

運行結果:示例中yield關鍵字右邊沒有表達式,所以該協程只從調用者那裏接受數據,yield產出返回給調用者的值爲None,即next(coro)獲取的是None。調用包含yield關鍵字的函數,得到的是一個生成器對象,首先調用next(coro)函數,因爲生成器還沒啓動,沒在yield語句處暫停,所以一直無法發送數據。調用這個方法後,協程定義體中的yield方法被掛起,控制權回到調用方,執行coro.send(...)後,協程會恢復,一直運行到下一個yield表達式或終止。此處,yield表達式將'this message form caller'賦值給x,控制權流動到協程定義體的末尾,導致生成器拋出StopIteration異常。

協程可身處4個狀態中的一個,當前狀態可使用inspect.getgeneratorstate(...)函數確定,該函數返回的字符串及含義如下:

inspect.getgeneratorstate(...)函數返回值 含義
GEN_CREATED 等待開始執行
GEN_RUNNING 解釋器正在執行(注:只有在多線程應用中才能看到這個狀態)
GEN_SUSPENDED 在yield表達式處暫停(注:因爲send方法的參數會作爲yield表達式的值,所以,僅當程序處於暫停狀態時才能調用send方法)
GEN_CLOSED 執行結束

一個簡單的例子來說明生成器(協程)的狀態:

from inspect import getgeneratorstate
from time import sleep


def corotine():
    for i in range(3):
        sleep(0.5)
        x = yield i+1
        print('-> corotine x=', x)


coro = corotine()
while True:
    try:
        print(f'corotine status:{getgeneratorstate(coro)}')
        coro.send(100)
        next(coro)
        print(f'corotine status:{getgeneratorstate(coro)}')
        next(coro)
        print(f'corotine status:{getgeneratorstate(coro)}')
    except TypeError as e:
        print(e)
        coro = corotine()
        next(coro)  # 激活生成器
        continue
    except StopIteration:
        print('coroutine is finished.')
        print(f'corotine status:{getgeneratorstate(coro)}')
        break

運行結果:進入循環後,生成器處於GEN_CREATED狀態,尚未激活,因此send(100)將觸發TypeError異常,異常信息非常清晰。最先調用next(coro)函數這一步通常視爲激活協程(即讓協程向前執行至第一個yield表達式,準備好作爲活躍的協程使用)。

下面再舉個例子,該示例定義一個產出兩個值的協程:

from inspect import getgeneratorstate


def sample_coro2(a):
    print('-> Started: a=', a)
    b = yield a
    print('-> Received: b=', b)
    c = yield a + b
    print('-> Received: c=', c)


coro = sample_coro2(2)
try:
    print(getgeneratorstate(coro))
    print('調用方:', next(coro))
    print(getgeneratorstate(coro))
    print('調用方:', coro.send(4))
    print('調用方:', coro.send(8))
except StopIteration:
    print(getgeneratorstate(coro))

運行結果:協程函數調用時並不會立即執行,所以首先打印的是GEN_CREATED,當執行next(coro)後,協程被激活,開始運行,執行到b=yield a後,暫停協程,讓步CPU的使用權給調用方;調用方此刻打印coro的狀態爲GEN_SUSPENDED狀態(即協程在yield表達式處暫停狀態)。繼續向下執行coro.send(4),又將調用權給到協程,協程表達式yield a的返回值即coro.send(4)發送的4,因此打印b=4。協程繼續向下執行到c=yield a+b,再次將CPU的使用權讓步給調用方,調用方獲取到yield a+b產出的值6。當協程函數執行到完畢,將拋出StopIteration異常,捕獲此異常,打印coro的狀態發現,協程已處於GEN_CLOSED狀態。

2.2 一個應用協程應用的例子

        下面我們介紹一個關於協程略微複雜的例子:使用協程計算平均值。

def average():
    total = 0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count

       這個無限循環表明,只要調用方不斷把值發給這個協程,它就會一直接收值,然後生成結果。僅當調用方在協程上調用.close()方法,或者沒有協程的引用而被垃圾回收程序回收時,這個協程纔會終止。

       這裏的yield表達式用於暫停執行協程,把結果發給調用方;還用於接收調用方後面發給協程的值,恢復無限循環。使用協程的好處是,total和count聲明爲局部變量即可,無需使用實例屬性或閉包在多次調用之間保持上下文。

運行結果:調用next(coro_ava)函數後,協程會向前執行到yield表達式,產出average變量的初始值---None,因此不會出現在控制檯中。此時,yield在協程表達式處暫停,等待調用方發送值。coro_ava.send(10)發送一個值,激活協程,把發送的值賦給term,並更新total、count、average三個變量的值,然後開始while循環的下一次迭代,產出average變量的值,等待下一次爲term變量賦值。

        介紹到這裏,細心的你可能迫切想知道如何終止averager實例,因爲定義體中有個無限循環。但在討論如何終止協程之前,我們要先知道如何啓動協程。使用協程之前必須預激,可是這一步很容易忘記。下一節,我們將在協程函數上使用一個特殊的裝飾器,來預激活協程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章