揭開 asyncio 的神秘面紗(2) - 協程就是生成器?
在第一篇文章『揭開 asyncio 的神秘面紗 - 從 hello world 說起』中, 我們提出一個問題:Python 協程和生成器行為非常類似,它們究竟是什麼關係? 在這篇文章中,我們就來探索、解決這個疑問。
萬事先 Google 一下:python coroutine generator。 我們可以搜到這個 PEP 342 —— Coroutines via Enhanced Generators,看這個 PEP 的狀態已經是 Final 了,說明官方已經接受了這個提議, 十有八九,Python 預設的協程就是基於
增強的生成器
來實現的。 繼續瀏覽 Google 搜尋結果,我們還可以看到一些 coroutine 相關的資料, 比如:PEP 492 —— Coroutines with async and await syntax。
這篇文章,我們就以這兩個 PEP 為參考資料,學習協程與生成器的聯絡與區別。
喵一眼 PEP 492 : 不止有 async/await 語法
我們喵一眼 PEP 492,可以發現,async/await 語法是在 Python 3。5 版本加入的(包含 3。5)。 而在 3。4 的時候,asyncio 就已經可以正常工作了,也就是說,3。4 版本也提供了一種方式來宣告協程:
>>>
@asyncio。coroutine
。。。
def
hello_world
():
。。。
yield
。。。
(
‘hello world’
)
。。。
>>>
g_coro
=
hello_world
()
>>>
g_coro
。
send
(
None
)
# 啟動生成器/協程
>>>
g_coro
。
send
(
None
)
# 恢復生成器
hello
world
Traceback
(
most
recent
call
last
):
File
“
,
line
1
,
in
<
module
>
StopIteration
從這個寫法,我們就很容易看出,以前的協程就是在生成器函式上套了個
asyncio。coroutine
的裝飾器。
這種使用裝飾器定義的協程叫做:
generator based coroutine
。 而使用 async/await 關鍵字定義的協程叫做
native coroutine
,這是新的也是更推薦的寫法。 這兩種寫法定義出來的協程本質是一樣的,只是叫法不一樣。
注:在 Python web 開發中,我們經常會提到另外一個非同步程式設計框架 gevent,gevent 的中 coroutine 是基於 greenlet 實現,greenlet 底層不是基於生成器的。在後續的文章中, 我們也會講 greenlet 和 generator 在實現層面的差異。
閱讀 PEP 342
在讀 PEP 342 之前,我們簡單瞭解下 PEP。
PEP,全名 Python Enhancement Proposal,當開發者想往 Python 新增大的新特性之前, 開發者需要寫一個 PEP 來詳細的介紹這個特性。一個 PEP 往往包含這幾個部分:
特性簡介(Abstract/Introduction)
為什麼要新增這個新特性(Motivation)
基本理論(Rationale)
新特性的一些細節(Specification)
。。。
下面,我們就來閱讀 PEP 342 的動機部分
它先描述了協程常見的一個使用場景
Coroutines are a natural way of expressing many algorithms, such as simulations, games, asynchronous I/O, and other forms of event-driven programming or co-operative multitasking。
接著說 Python 的生成器功能已經很接近協程(言下之意是功能上還差點)
Python‘s generator functions are almost coroutines —— but not quite
然後講了現在的生成器功能差在哪裡
in that they allow pausing execution to produce a value, but do not provide for values or exceptions to be passed in when execution resumes。 They also do not allow execution to be paused within the try portion of try/finally blocks, and therefore make it difficult for an aborted coroutine to clean up after itself。
現在的生成器雖然可以在暫停執行時吐出一個值,但是恢復生成器時,我們不能傳入引數。 (言下之意是恢復協程時,應該需要支援傳入引數)
現在的生成器不支援在 try block 中暫停(言下之意是協程應該要支援在 try block 中暫停)
讀完這段文字,相信我們自己可以回答這麼兩個問題:
瞭解為什麼 Python 的協程會基於生成器實現?
實現協程前,需要對生成器作什麼改進?
說了這麼多次協程,但是我們似乎還沒有說過一個最基本的概念:什麼是協程?
什麼是協程?
coroutine 的定義有很多,Python 文件是這麼寫的:
Coroutines is a more generalized form of subroutines。 Subroutines are entered at one point and exited at another point。 Coroutines can be entered, exited, and resumed at many different points。
而 Unity 文件是這麼寫的:
A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes。
解讀一下這兩個定義:
一個協程是一個函式/子程式(可以認為函式和子程式是指一個東西)。這個函式可以暫停執行, 把執行權讓給 YieldInstruction,等 YieldInstruction 執行完成後,這個函式可以繼續執行。 這個函式可以多次這樣的暫停與繼續。
注:這裡的 YieldInstruction, 我們其實也可以簡單理解為函式。
下面,我們就來看一些例子,來幫助我們理解這個概念。
像普通函式一樣
協程可以像普通函式一樣,一個協程呼叫另外一個協程並等待它返回。
>>>
import
asyncio
>>>
>>>
async
def
hello_world
():
。。。
(
’hello world‘
)
。。。
>>>
async
def
job1
():
。。。
(
’job1 started。。。‘
)
。。。
(
’job1 paused‘
)
。。。
await
hello_world
()
。。。
(
’job1 resumed‘
)
。。。
(
’job1 finished‘
)
。。。
>>>
loop
=
asyncio
。
get_event_loop
()
>>>
loop
。
run_until_complete
(
job1
())
job1
started
。。。
job1
paused
hello
world
job1
resumed
job1
finished
job1 呼叫 hello_world 並等待它返回。
比普通函式厲害
協程可以在“卡住”的時候可以幹其它事情。
>>>
async
def
long_task
():
。。。
(
’long task started‘
)
。。。
await
asyncio
。
sleep
(
1
)
。。。
(
’long task finished‘
)
。。。
>>>
loop
。
create_task
(
long_task
())
<
Task
pending
coro
=<
long_task
()
running
at
<
stdin
>
:
1
>>
>>>
loop
。
create_task
(
job1
())
>>>>>
loop
。
create_task
(
job1
())
<
Task
pending
coro
=<
job1
()
running
at
<
stdin
>
:
1
>>
>>>
>>>
try
:
。。。
loop
。
run_forever
()
。。。
except
KeyboardInterrupt
:
。。。
pass
。。。
long
task
started
job1
started
。。。
job1
paused
hello
world
job1
resumed
job1
finished
long
task
finished
^
C
>>>
從這段程式的輸出可以看出,程式本來是在執行 long task 協程,但由於 long task 要 await sleep 1 秒,於是 long task
自動
暫停了,hello_world 協程自動開始執行, hello world 執行完之後,long task 繼續執行。
生成器的暫停與恢復
協程可以暫停和恢復,生成器當然就也可以。下面來看一個生成器暫停、恢復的例子:
>>> def gen():
。。。 print(’generator started‘)
。。。 print(’generator paused‘)
。。。 yield
。。。 print(’generator resumed‘)
。。。 print(’generator paused, again‘)
。。。 yield
。。。 print(’generator resumed‘)
。。。 print(’generator finished‘)
。。。
>>> def main():
。。。 g = gen()
。。。 print(’generator created‘)
。。。 print(’————————-‘)
。。。 g。send(None) # start generator
。。。 print(’————————-‘)
。。。 g。send(None) # resume generator
。。。 print(’————————-‘)
。。。 g。send(None) # resume generator
。。。
>>> main()
generator created
————————-
generator started
generator paused
————————-
generator resumed
generator paused, again
————————-
generator resumed
generator finished
Traceback (most recent call last):
File “
File “
StopIteration
小結
在這篇文章中,我們以兩個 PEP 為主要參考資料,瞭解到協程有兩種定義的方法, 其中使用生成器形式定義的協程叫做
generator-based coroutine
, 透過 async/await 宣告的協程叫做
native coroutine
,兩者底層實現都是生成器。接著, 我們闡述了協程的概念,從概念和例子出發,講了協程和生成器最主要的特徵:可以暫停執行和恢復執行。
至於標題中的問題:協程就是生成器?我想我們或許可以這樣回答:coroutine is not geneartor but coroutine equals to (enhanced) generator。
話說看完本問,你有木有好奇:為了實現協程,生成器作了哪些增強呢?這個問題留給讀者哈 ~ 閱讀 PEP 342 —— Coroutines via Enhanced Generators 即可找到答案。
下篇文章,我們來探討 Python 生成器的實現原理。