您當前的位置:首頁 > 詩詞

揭開 asyncio 的神秘面紗(2) - 協程就是生成器?

作者:由 Cosven 發表于 詩詞時間:2019-03-11

在第一篇文章『揭開 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

。。。

print

‘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

():

。。。

print

’hello world‘

。。。

>>>

async

def

job1

():

。。。

print

’job1 started。。。‘

。。。

print

’job1 paused‘

。。。

await

hello_world

()

。。。

print

’job1 resumed‘

。。。

print

’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

():

。。。

print

’long task started‘

。。。

await

asyncio

sleep

1

。。。

print

’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 “”, line 1, in

File “”, line 9, in main

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 生成器的實現原理。

標簽: 協程  生成器  generator  print  PEP