程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

一文講透 python 協程

編輯:Python

1. 引言

上一篇文章中,我們介紹了 Python 中的 yield 關鍵字以及依賴其實現的生成器函數。 python 中的迭代器與生成器

生成器函數在形式上與協程已經十分接近,本文我們就來詳細介紹一下協程。

2. 協程

協程又稱為微線程,雖然整個執行過程中只有一個線程,但某個方法的執行過程中可以掛起、讓出CPU給另一個方法,等到適當的時機再回到原方法繼續執行,但兩個方法之間並沒有相互調用關系,他們類似於系統中斷或多線程的表現。 由此,我們可以看到協程具有以下優勢:

  1. 執行效率高,通過執行中的切換,讓多個方法近乎同時執行,減少IO等待,有效提升了執行效率
  2. 性能優於多線程,對於多線程並發的程序設計,多個線程切換過程中需要消耗一定的時間,而協程切換的時間消耗則十分微小,並且隨著並發量越大優勢越明顯
  3. 編程相對簡單,因為協程中的多個方法均在同一個線程中,所以協程中沒有競爭條件,不需要考慮加鎖

2.1. 示例

>>> def simple_coroutine():
... print('-> coroutine started')
... x = yield
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration

可以看到,在上面的例子中,yield 不再是我們所熟悉的出現在式子的左邊,而是成為了變量賦值的右值,事實上,此處 yield 右側同樣可以出現值、變量或表達式。 當程序執行到 yield 表達式時,協程被掛起,同時返回 yield 右側的值(如果有的話) 對這個協程執行 send 操作實際上就是將 send 方法的參數傳遞給 yield 表達式的左值,接著程序繼續運行下去。

3. 協程的狀態

協程有以下四種狀態:

  1. GEN_CREATED — 等待開始執行
  2. GEN_RUNNING — 正在執行
  3. GEN_SUSPENDED — 在 yield 表達式處暫停
  4. GEN_CLOSED — 執行結束

通過使用 inspect.getgeneratorstate 函數可以返回上述四個中的一個狀態字符串。 只有當一個協程處於 GEN_SUSPENDED 狀態時才可以調用其 send 方法,否則會拋出異常:

>>> my_coro = simple_coroutine()
>>> my_coro.send(1729)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

3.1. 協程的執行

>>> def simple_coro2(a):
... print('-> Started: a =', a)
... b = yield a
... print('-> Received: b =', b)
... c = yield a + b
... print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'

下圖展示了上述代碼的執行過程:

3.2. 預激

因此需要首先調用 next 方法,讓協程執行到第一個 yield 表達式,這一過程被稱為“預激”(prime) 所有協程都必須預激然後使用,這一次 next 調用看上去總是讓人覺得有些多余,而沒有他又會報錯,所以我們可以在自己的協程上加一個裝飾器,以使協程被創建後自動完成預激功能。

from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return primer

關於裝飾器的內容,可以參考: python 中的裝飾器及其原理

3.3. 關閉

有下面幾種情況會讓協程進入 GEN_CLOSED 狀態:

  1. 與迭代器、生成器函數一樣,當我們不斷執行 next 方法或 send 方法讓所有 yield 表達式依次被執行,直到最後一個 yield 表達式被執行後,就會拋出 StopIteration 異常,此時協程進入 GEN_CLOSED 狀態
  2. 協程同時提供了 close 方法,無論協程處於什麼狀態,close 方法可以立即讓協程進入 GEN_CLOSED 狀態
  3. 如果協程運行中出現未捕獲異常,異常首先會傳遞給 next 或 send 方法拋出,協程也將終止
  4. 你也可以調用 throw 方法主動將一個異常傳遞給協程並拋出,達到讓協程拋出異常並關閉協程的目的,事實上 close 方法也是通過讓協程拋出 GeneratorExit 異常實現的
  5. 還有一種情況會使協程進入 GEN_CLOSED 狀態,那就是當沒有任何引用指向他時被回收

關於 Python 的垃圾回收機制,參考: python 的內存管理與垃圾收集

3.4. 示例 — 利用協程計算移動平均數

from collections import namedtuple
Result = namedtuple('Result', 'count average')
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count

可以看到,上例中,協程是一個無限循環,只要調用方不斷將值發送給協程,他就會不斷累加、計算移動平均數,直到協程的 close 方法被調用或協程對象被垃圾回收。

4. 協程的 return

下面的例子中,我們在上面計算移動平均數的代碼最後加上了返回語句。

from collections import namedtuple
Result = namedtuple('Result', 'count average')
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Result(count, average)
>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> coro_avg.send(None)
Traceback (most recent call last):
...
StopIteration: Result(count=3, average=15.5)

可以看到,在最終給協程發送 None 導致協程退出後,拋出的 StopIteration 中攜帶了這個返回值,通過 StopIteration 的 value 字段我們可以取出該值:

5. 委派生成器 — yield from

yield from 語句可以簡化生成器函數中的 yield 表達式,這在我們此前的文章中已經介紹過:

>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
['A', 'B', 1, 2]

可以改寫成:

>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

包含 yield from 語句的函數被稱為委派生成器,他打開了雙向通道,將最外層的調用方與最內層的子生成器連接起來,讓二者可以直接發送和產出值,還可以直接傳入異常,位於中間的協程無序添加任何中間處理的代碼。 yield from 語句會一直等待子生成器終止並拋出 StopIteration 異常,而子生成器通過 return 語句返回的值會成為 yield from 語句的傳入值。


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved