Python3中asyncio異步詳解一

在學(xué)習(xí)asyncio相關(guān)的知識(shí)前,如果有同學(xué)沒有讀到Python3中yield與yield from詳解的話,還請(qǐng)先瀏覽一下,以便能對(duì)asyncio有更好的理解。至于如何學(xué)習(xí)asyncio呢?我想從以下幾個(gè)方面來(lái)闡述:

一:asyncio工作原理和重要概念
????????1.1:?事件循環(huán) -??Eventloop
? ? ? ? 1.2:?協(xié)程對(duì)象 -??Coroutine
? ? ? ? 1.3:?Future & Task 對(duì)象
? ? ? ? 1.4:?async/await 原生協(xié)程
? ? ? ? 1.5:?asyncio 如何正確啟動(dòng)

一:?asyncio工作原理和重要概念

那么在學(xué)習(xí)asyncio之前,有個(gè)插曲,其實(shí)你應(yīng)該要知道操作系統(tǒng)中的網(wǎng)絡(luò)編程的相關(guān)知識(shí),比如阻塞IO、非阻塞IO、IO多路復(fù)用、異步IO等,具體可參考?Linux IO模式及 select、poll、epoll詳解,對(duì)其有個(gè)大概的理解,方便我們對(duì)asyncio的理解。下面我們將講解幾個(gè)asyncio 相關(guān)的概念問(wèn)題:

1.1:事件循環(huán) -??Eventloop

Eventloop 可以說(shuō)是 asyncio 應(yīng)用的核心,是中央總控。Eventloop 實(shí)例提供了注冊(cè)、取消和執(zhí)行任務(wù)和回調(diào)的方法。程序開啟一個(gè)無(wú)限的循環(huán),程序員會(huì)把一些函數(shù)(協(xié)程)注冊(cè)到事件循環(huán)上。當(dāng)滿足事件發(fā)生的時(shí)候,調(diào)用相應(yīng)的協(xié)程函數(shù)。

把一些異步函數(shù) (就是任務(wù),Task,一會(huì)就會(huì)說(shuō)到) 注冊(cè)到這個(gè)事件循環(huán)上,事件循環(huán)會(huì)循環(huán)執(zhí)行這些函數(shù) (但同時(shí)只能執(zhí)行一個(gè)),當(dāng)執(zhí)行到某個(gè)函數(shù)時(shí),如果它正在等待 I/O 返回,事件循環(huán)會(huì)暫停它的執(zhí)行去執(zhí)行其他的函數(shù);當(dāng)某個(gè)函數(shù)完成 I/O 后會(huì)恢復(fù),下次循環(huán)到它的時(shí)候繼續(xù)執(zhí)行。因此,這些異步函數(shù)可以協(xié)同 (Cooperative) 運(yùn)行:這就是事件循環(huán)的目標(biāo)。

(1): 事件循環(huán)是執(zhí)行我們的異步代碼并決定如何在異步函數(shù)之間切換的對(duì)象。如果某個(gè)協(xié)程在等待某些資源,我們需要暫停它的執(zhí)行,在事件循環(huán)中注冊(cè)這個(gè)事件,以便當(dāng)事件發(fā)生的時(shí)候,能再次喚醒該協(xié)程的執(zhí)行。
(2): 運(yùn)行異步函數(shù)我們首先需要?jiǎng)?chuàng)建一個(gè)協(xié)程,然后創(chuàng)建future或task對(duì)象,將它們添加到事件循環(huán)中,到目前為止,我們的異步函數(shù)中沒有任何代碼被執(zhí)行過(guò),只有調(diào)用loop.run_until_completed啟動(dòng)事件循環(huán),才會(huì)開始執(zhí)行future或task對(duì)象,loop.run_until_completed會(huì)阻塞程序直到所有的協(xié)程對(duì)象都執(zhí)行完畢。
備注:事件循環(huán)的本質(zhì)就是通過(guò) await Coroutinec_or_task_?or_future 將程序的控制權(quán)還給CPU, 然后CPU執(zhí)行其它的任務(wù),當(dāng)再次遇到 await xxx,CPU又獲取到了控制權(quán),再次選擇執(zhí)行其它任務(wù)。當(dāng)某個(gè)任務(wù)(或稱事件)完成了,喚醒CPU, 讓其獲取控制權(quán)執(zhí)行后面的代碼。


1、事件循環(huán)是在線程中執(zhí)行
2、從隊(duì)列中取得任務(wù)
3、每個(gè)任務(wù)在協(xié)程中執(zhí)行下一步動(dòng)作
4、如果在一個(gè)協(xié)程中調(diào)用另一個(gè)協(xié)程(await?),會(huì)觸發(fā)上下文切換,掛起當(dāng)前協(xié)程,并保存現(xiàn)場(chǎng)環(huán)境(變量,狀態(tài)),然后載入被調(diào)用協(xié)程
5、如果協(xié)程的執(zhí)行到阻塞部分(阻塞I/O,Sleep),當(dāng)前協(xié)程會(huì)掛起,并將控制權(quán)返回到線程的消息循環(huán)中,然后消息循環(huán)繼續(xù)從隊(duì)列中執(zhí)行下一個(gè)任務(wù)...以此類推
6、隊(duì)列中的所有任務(wù)執(zhí)行完畢后,消息循環(huán)返回第一個(gè)任務(wù)

? 1.2:?協(xié)程對(duì)象 -??Coroutine

協(xié)程對(duì)象,指一個(gè)使用async關(guān)鍵字定義的函數(shù),它的調(diào)用不會(huì)立即執(zhí)行函數(shù),而是會(huì)返回一個(gè)協(xié)程對(duì)象。協(xié)程對(duì)象需要注冊(cè)到事件循環(huán),由事件循環(huán)調(diào)用。協(xié)程 (Coroutine) 本質(zhì)上是一個(gè)函數(shù).

1:async 關(guān)鍵定義的函數(shù)(稱協(xié)程),關(guān)鍵字await 只能在協(xié)程內(nèi)部中使用,?其它地方不能使用
2:協(xié)程內(nèi)部不能使用 yield from,報(bào)語(yǔ)法錯(cuò)誤,但可以使用 yield (但很少使用, 自python3.7之后只有使用 async/await定義的函數(shù)才是原生協(xié)程,會(huì)逐漸拋棄yield與asyncio.coroutine這種zai Python 3.5裝飾器生成的協(xié)程)
3:被 async 關(guān)鍵定義的協(xié)程,實(shí)際上是一個(gè)Coroutine對(duì)象,而Coroutine又繼承Awaitable?
????????from collections.abc import Coroutine, Awaitable
4:定義協(xié)程示例:
????????async? def? async_test():
? ? ? ? ????????await asyncio.sleep(2)
? ? ? ? ????????......

Awaitable 對(duì)象: await?關(guān)鍵字用于將程序控制權(quán)移交給事件循環(huán)并中斷當(dāng)前協(xié)程的執(zhí)行。它有以下幾個(gè)使用規(guī)則:

a: 只能用在由?async def?修飾的函數(shù)中,在普通函數(shù)中使用會(huì)拋出異常。
b: 調(diào)用一個(gè)協(xié)程函數(shù)后,就必須等待其執(zhí)行完成并返回結(jié)果。
c: await func()?中的?func()?必須是一個(gè)?awaitable?對(duì)象。即一個(gè)協(xié)程函數(shù)或者一個(gè)在內(nèi)部實(shí)現(xiàn)了?__await__()?方法的對(duì)象,該方法會(huì)返回一個(gè)生成器

Awaitable 對(duì)象包含協(xié)程、Task 和 Future 等。

? 1.3:??Future & Task 對(duì)象

Future 對(duì)象:?

代表將來(lái)執(zhí)行或沒有執(zhí)行的任務(wù)的結(jié)果,它和task上沒有本質(zhì)的區(qū)別。

它代表了一個(gè)「未來(lái)」對(duì)象,異步操作結(jié)束后會(huì)把最終結(jié)果設(shè)置到這個(gè) Future 對(duì)象上。Future 是對(duì)協(xié)程的封裝,不過(guò)日常開發(fā)基本是不需要直接用這個(gè)底層 Future 類的。

Future對(duì)象封裝了一個(gè)未來(lái)會(huì)被計(jì)算的可調(diào)用的異步執(zhí)行對(duì)象,他們能被放入隊(duì)列,他們的狀態(tài)、結(jié)果或者異常能被查詢。 Future對(duì)象有一個(gè)result屬性,用于存放未來(lái)的執(zhí)行結(jié)果。還有個(gè)set_result()方法,是用于設(shè)置result的,并且會(huì)在給result綁定值以后運(yùn)行事先給Future對(duì)象添加的回調(diào)。回調(diào)是通過(guò)Future對(duì)象的add_done_callback()方法添加的。

重要的是Future對(duì)象不能被我們創(chuàng)建,只能被異步框架創(chuàng)建,有兩種方法:

# 該函數(shù)在 Python 3.7 中被加入,更加高層次的函數(shù),返回Task對(duì)象

future1= asyncio.create_task(my_coroutine)

# 在Python 3.7 之前,是更加低級(jí)的函數(shù),返回Future對(duì)象或者Task對(duì)象

future2= asyncio.ensure_future(my_coroutine)

第一種方法在循環(huán)中添加一個(gè)協(xié)程并返回一個(gè)task對(duì)象,task對(duì)象是future的子類型。第二種方法非常相似,當(dāng)傳入?yún)f(xié)程對(duì)象時(shí)返回一個(gè)Task對(duì)象,唯一的區(qū)別是它也可以接受Future對(duì)象或Task對(duì)象,在這種情況下它不會(huì)做任何事情并且返回Future對(duì)象或者Task對(duì)象不變。

Future對(duì)象有幾個(gè)狀態(tài):

Pending:就緒

Running:運(yùn)行

Done:完成

Cancelled:取消

創(chuàng)建Future對(duì)象的時(shí)候,狀態(tài)為pending,事件循環(huán)調(diào)用執(zhí)行的時(shí)候就是running,調(diào)用完畢就是done,如果需要取消Future對(duì)象的調(diào)度執(zhí)行,可調(diào)用Future對(duì)象的cancel()函數(shù)。

除此之外,F(xiàn)uture對(duì)象還有下面一些常用的方法:

result():立即返回Future對(duì)象運(yùn)行結(jié)果或者拋出執(zhí)行時(shí)的異常,沒有timeout參數(shù),如果Future沒有完成,不會(huì)阻塞等待結(jié)果,而是直接拋出InvalidStateError異常。最好的方式是通過(guò)await獲取運(yùn)行結(jié)果,await會(huì)自動(dòng)等待Future完成返回結(jié)果,也不會(huì)阻塞事件循環(huán),因?yàn)樵赼syncio中,await被用來(lái)將控制權(quán)返回給事件循環(huán)。

done():非阻塞的返回Future對(duì)象是否成功取消或者運(yùn)行結(jié)束或被設(shè)置異常,而不是查看future是否已經(jīng)執(zhí)行完成。

cancelled():判斷Future對(duì)象是否被取消。

add_done_callback():傳入一個(gè)可回調(diào)對(duì)象,當(dāng)Future對(duì)象done時(shí)被調(diào)用。

exception():獲取Future對(duì)象中的異常信息,只有當(dāng)Future對(duì)象done時(shí)才會(huì)返回。

get_loop():獲取當(dāng)前Future對(duì)象綁定的事件循環(huán)。

需要注意的是,當(dāng)在協(xié)程內(nèi)部引發(fā)未處理的異常時(shí),它不會(huì)像正常的同步編程那樣破壞我們的程序,相反,它存儲(chǔ)在future內(nèi)部,如果在程序退出之前沒有處理異常,則會(huì)出現(xiàn)以下錯(cuò)誤:

Task? exception? was? never? retrieved

有兩種方法可以解決此問(wèn)題,在訪問(wèn)future對(duì)象的結(jié)果時(shí)捕獲異?;蛘{(diào)用future對(duì)象的異常函數(shù):

try:
? ? ? ? # 調(diào)用結(jié)果時(shí)捕獲異常
? ? ? ? my_promise.result()
catchException:
????????pass
# 獲取在協(xié)程執(zhí)行過(guò)程中拋出的異常
my_promise.exception()

Task對(duì)象

一個(gè)協(xié)程對(duì)象就是一個(gè)原生可以掛起的函數(shù),任務(wù)則是對(duì)協(xié)程進(jìn)一步封裝,其中包含任務(wù)的各種狀態(tài)。Task 對(duì)象是 Future 的子類,它將 coroutine 和 Future 聯(lián)系在一起,將 coroutine 封裝成一個(gè) Future 對(duì)象。

Future 是協(xié)程的封裝,F(xiàn)uture 對(duì)象提供了很多任務(wù)方法 (如完成后的回調(diào)、取消、設(shè)置任務(wù)結(jié)果等等),但是開發(fā)者并不需要直接操作 Future 這種底層對(duì)象,而是用 Future 的子類 Task 協(xié)同的調(diào)度協(xié)程以實(shí)現(xiàn)并發(fā)。

Task對(duì)象被用來(lái)在事件循環(huán)中運(yùn)行協(xié)程。如果一個(gè)協(xié)程在等待一個(gè)Future對(duì)象,Task對(duì)象會(huì)掛起該協(xié)程的執(zhí)行并等待該Future對(duì)象完成。當(dāng)該Future對(duì)象完成,被暫停的協(xié)程將恢復(fù)執(zhí)行。事件循環(huán)使用協(xié)作調(diào)度: 一個(gè)事件循環(huán)每次運(yùn)行一個(gè)Task對(duì)象。當(dāng)一個(gè)Task對(duì)象等待一個(gè)Future對(duì)象完成時(shí),該事件循環(huán)會(huì)運(yùn)行其他Task、回調(diào)或執(zhí)行IO操作。

使用高層級(jí)的asyncio.create_task()函數(shù)來(lái)創(chuàng)建Task對(duì)象,也可用低層級(jí)的loop.create_task()或ensure_future()函數(shù)。不建議手動(dòng)實(shí)例化 Task 對(duì)象。

Python 的例子:

import asyncio

import time

async? def? compute(x, y):
????????print("Compute {} + {}...".format(x, y))
? ? ? ? await? asyncio.sleep(2.0)
? ? ? ? return x+y

async? def? print_sum(x, y):
? ? ? ? result =await compute(x, y)
? ? ? ? print("{} + {} = {}".format(x, y, result))

start = time.time()?????
loop = asyncio.get_event_loop()?
tasks = [?
? ? ? ? asyncio.ensure_future(print_sum(0,0)),
? ? ? ? asyncio.ensure_future(print_sum(1,1)),
? ? ? ? asyncio.ensure_future(print_sum(2,2)),
]
loop.run_until_complete(asyncio.wait(tasks))?
loop.close()?
print("Total elapsed time {}".format(time.time() - start))

上面的代碼的執(zhí)行流程是:

詳細(xì)的流程如下:

?1.4:?async/await 原生協(xié)程

Python設(shè)計(jì)者們?cè)?3.5 中新增了async/await語(yǔ)法(PEP 492),將協(xié)程作為原生Python語(yǔ)言特性,并且將他們與生成器明確的區(qū)分開。它避免了生成器/協(xié)程中間的混淆,方便編寫出不依賴于特定庫(kù)的協(xié)程代碼,稱之為原生協(xié)程。async/await 和 yield from這兩種風(fēng)格的協(xié)程底層復(fù)用共同的實(shí)現(xiàn),而且相互兼容。在Python 3.6 中asyncio庫(kù)“轉(zhuǎn)正”,不再是實(shí)驗(yàn)性質(zhì)的,成為標(biāo)準(zhǔn)庫(kù)的正式一員。

Python 3.5 添加了types.coroutine修飾器,也可以像 asyncio.coroutine 一樣將生成器標(biāo)記為協(xié)程。你可以用 async def 來(lái)定義一個(gè)協(xié)程函數(shù),雖然這個(gè)函數(shù)不能包含任何形式的 yield 語(yǔ)句;只有 return 和 await 可以從協(xié)程中返回值。

?* 句法?async def?引入了原生協(xié)程或者說(shuō)異步生成器。表達(dá)式?async with?和?async for?也是允許的,稍后就可以看到。

* 關(guān)鍵字?await?將控制器傳遞給時(shí)間循環(huán)。(掛起當(dāng)前運(yùn)行的協(xié)程。)Python執(zhí)行的時(shí)候,在g()?函數(shù)范圍內(nèi)如果遇到表達(dá)式?await f(),就是?await?在告訴事件循環(huán)“掛起?g()?函數(shù),直到?f()?返回結(jié)果,在此期間,可以運(yùn)行其他函數(shù)?!?/p>

上述第二點(diǎn)在代碼中大致如下:

async def g():
? ? ? ? # 暫停,知道f()在返回到g()
? ? ? ? await? f()

關(guān)于要不要用?async/await,以及何時(shí)使用,如何使用,都有一套嚴(yán)格的規(guī)則。無(wú)論你是在使用語(yǔ)法還是已經(jīng)使用?async/await,這些規(guī)則都會(huì)很方便:

1. 協(xié)程是引入了?async def?的函數(shù)。你可能會(huì)用到?await,return?或者?yield,但是這些都是可選的。Python允許使用?async def noop(): pass?聲明:
????????1.1. 使用?await?與?return?的組合創(chuàng)建協(xié)程函數(shù)。想要調(diào)用一個(gè)協(xié)程函數(shù),必須使用?await等待返回結(jié)果。
????????1.2. 在?async def?代碼塊中使用?yield?的情況并不多見(只有Python的近期版本才可用)。當(dāng)你使用?async for?進(jìn)行迭代的時(shí)候,會(huì)創(chuàng)建一個(gè)異步生成器。暫時(shí)先忘掉異步生成器,將目光放在使用?await?與?return?的組合創(chuàng)建協(xié)程函數(shù)的語(yǔ)法上。
????????1.3. 在任何使用?async def?定義的地方都不可以使用?yield from,這會(huì)引發(fā)異常?SyntaxError。

2. 一如在?def?定義的函數(shù)之外使用?yield?會(huì)引發(fā)異常?SyntaxError,在?async def?定義的協(xié)程之外使用?await?也會(huì)引發(fā)異常?SyntaxError。你只能在協(xié)程內(nèi)部使用?await。

這里有一個(gè)簡(jiǎn)介的例子,總結(jié)了上面的幾條規(guī)則:

最后,當(dāng)你使用?await f()?時(shí),要求?f()?是一個(gè)可等待的對(duì)象。但這并沒有什么用。現(xiàn)在,只需要知道可等待對(duì)象要么是(1)其他的協(xié)程,要么就是(2)定義了?.await()?函數(shù)且返回迭代器的對(duì)象。如果你正在編寫程序,絕大多數(shù)情況只需要關(guān)注案例#1。

這給我們帶來(lái)了一些技術(shù)上的差異:將一個(gè)函數(shù)標(biāo)記為協(xié)程的舊的一個(gè)方式是使用?@asyncio.coroutine?裝飾一個(gè)普通的函數(shù)。這是基于生成器的協(xié)程。但是這種方式自Python 3.5中出現(xiàn)了?async/await?語(yǔ)法后就已經(jīng)過(guò)時(shí)了。

下面兩個(gè)協(xié)程基本上是等價(jià)的(都是可等待的),但第一個(gè)是基于生成器的,而第二個(gè)是原生協(xié)程。

如果你寫代碼的時(shí)候更趨向于顯式聲明而不是隱式聲明,那么最好是使用原生協(xié)程?;谏善鞯膮f(xié)程將會(huì)在Python 3.10版本移除

本教程的后半部分,我們會(huì)再涉及一些基于生成器的協(xié)程的優(yōu)點(diǎn)。為了使協(xié)程成為Python中獨(dú)立的標(biāo)準(zhǔn)功能,并與常規(guī)生成器區(qū)分開,以減少歧義,Python引入?async/await。

不要沉迷于基于生成器的協(xié)程,它已經(jīng)被?async/await?取代了。如果你要使用?async/await?語(yǔ)法的話,注意它的一些特有的規(guī)則(比如,await?不能用于基于生成器的協(xié)程),這些規(guī)則很大程度上與基于生成器的協(xié)程不兼容。

協(xié)程的主要屬性包括:

1:async def函數(shù)始終為協(xié)程,即使它不包含await表達(dá)式。

2:如果在async函數(shù)中使用yield或者yield from表達(dá)式會(huì)產(chǎn)生SyntaxError錯(cuò)誤。

3:在內(nèi)部,引入了兩個(gè)新的代碼對(duì)象標(biāo)記:
????????CO_COROUTINE用于標(biāo)記原生協(xié)程(和新語(yǔ)法一起定義)
????????CO_ITERABLE_COROUTINE用于標(biāo)記基于生成器的協(xié)程,兼容原生協(xié)程。(通過(guò)types.coroutine()函數(shù)設(shè)置)

4:常規(guī)生成器在調(diào)用時(shí)會(huì)返回一個(gè)genertor對(duì)象,同理,協(xié)程在調(diào)用時(shí)會(huì)返回一個(gè)coroutine對(duì)象。

5:協(xié)程不再拋出StopIteration異常,而是替代為RuntimeError。常規(guī)生成器實(shí)現(xiàn)類似的行為需要進(jìn)行引入future(PEP-3156)

6:當(dāng)協(xié)程進(jìn)行垃圾回收時(shí),一個(gè)從未被await的協(xié)程會(huì)拋出RuntimeWarning異常

types.coroutine():在types模塊中新添加了一個(gè)函數(shù)coroutine(fn)用于asyncio中基于生成器的協(xié)程與本PEP中引入的原生攜協(xié)程互通。使用它,“生成器實(shí)現(xiàn)的協(xié)程”和“原生協(xié)程”之間可以進(jìn)行互操作。

@types.coroutine
def? process_data(db):
????????data =yieldfromread_data(db)
? ? ? ? ...

這個(gè)函數(shù)將生成器函數(shù)對(duì)象設(shè)置CO_ITERABLE_COROUTINE標(biāo)記,將返回對(duì)象變?yōu)閏oroutine對(duì)象。如果fn不是一個(gè)生成器函數(shù),那么它會(huì)對(duì)其進(jìn)行封裝。如果它返回一個(gè)生成器,那么它會(huì)封裝一個(gè)awaitable代理對(duì)象。

注意:CO_COROUTINE標(biāo)記不能通過(guò)types.coroutine()進(jìn)行設(shè)置,這就可以將新語(yǔ)法定義的原生協(xié)程與基于生成器的協(xié)程進(jìn)行區(qū)分。

await與yield from相似,await關(guān)鍵字的行為類似標(biāo)記了一個(gè)斷點(diǎn),掛起協(xié)程的執(zhí)行直到其他awaitable對(duì)象完成并返回結(jié)果數(shù)據(jù)。它復(fù)用了yield from的實(shí)現(xiàn),并且添加了額外的驗(yàn)證參數(shù)。await只接受以下之一的awaitable對(duì)象:

一個(gè)原生協(xié)程函數(shù)返回的原生協(xié)程對(duì)象。

一個(gè)使用types.coroutine()修飾器的函數(shù)返回的基于生成器的協(xié)程對(duì)象。這種用法已經(jīng)被廢棄

一個(gè)包含返回迭代器的await方法的對(duì)象。

協(xié)程鏈:協(xié)程的一個(gè)關(guān)鍵特性是它們可以組成協(xié)程鏈,就像函數(shù)調(diào)用鏈一樣,一個(gè)協(xié)程對(duì)象是awaitable的,因此其他協(xié)程可以await另一個(gè)協(xié)程對(duì)象。

任意一個(gè)yield from鏈都會(huì)以一個(gè)yield結(jié)束,這是Future實(shí)現(xiàn)的基本機(jī)制。因此,協(xié)程在內(nèi)部中是一種特殊的生成器。每個(gè)await最終會(huì)被await調(diào)用鏈條上的某個(gè)yield語(yǔ)句掛起。

關(guān)于基于生成器的協(xié)程和async定義的原生協(xié)程之間的差異,關(guān)鍵點(diǎn)是只有基于生成器的協(xié)程可以真正的暫停執(zhí)行并強(qiáng)制性返回給事件循環(huán)。所以每個(gè)await最終會(huì)被await調(diào)用鏈條上的某個(gè)由types.coroutine()裝飾的包含yield語(yǔ)句的協(xié)程函數(shù)掛起。

為了啟用協(xié)程的這一特點(diǎn),一個(gè)新的魔術(shù)方法__await__被添加進(jìn)來(lái)。在asyncio中,對(duì)于對(duì)象在await語(yǔ)句啟用Future對(duì)象只需要添加await?=?iter這行到asyncio.Future類中。帶有await方法的對(duì)象也叫做Future-like對(duì)象。

另外還新增了異步上下文管理 async with 和異步迭代器 async for。異步生成器和異步推導(dǎo)式都讓迭代變得并發(fā),他們所做的只是提供同步對(duì)應(yīng)的外觀,但是有問(wèn)題的循環(huán)能夠放棄對(duì)事件循環(huán)的控制,以便運(yùn)行其他協(xié)程。

關(guān)于何時(shí)以及如何能夠和不能使用async / await,有一套嚴(yán)格的規(guī)則:

使用async關(guān)鍵字創(chuàng)建一個(gè)協(xié)程函數(shù),里面包含await或者return,調(diào)用協(xié)程函數(shù),必須使用await獲得函數(shù)返回結(jié)果。

在async異步函數(shù)中使用yield并不常見,這會(huì)創(chuàng)建一個(gè)異步生成器,可以使用async for來(lái)迭代異步生成器。

在async異步函數(shù)中使用yield from會(huì)拋出語(yǔ)法錯(cuò)誤。同樣在普通函數(shù)中使用await也是語(yǔ)法錯(cuò)誤。

Python異步編程詳解

Python 3.5中async/await的工作機(jī)制

Python Async/Await入門指南

? 1.5:?asyncio 如何正確啟動(dòng)

協(xié)程完整的工作流程是這樣的

定義/創(chuàng)建協(xié)程對(duì)象

將協(xié)程轉(zhuǎn)為task任務(wù)

定義事件循環(huán)對(duì)象容器

將task任務(wù)扔進(jìn)事件循環(huán)對(duì)象中觸發(fā)

1.5.1 錯(cuò)誤姿勢(shì):看似是異步,其實(shí)是同步

async def coro_a():? ? ????
????????print("Suspending coro_a")????????
????????await asyncio.sleep(3)????????
????????print("running coro_a")????????
????????return 100

async def coro_b():????
????????print("Suspending coro_b")???
????????await asyncio.sleep(1)????
????????print("running coro_b")????
????????return [1, 23, 45]

async def sync_run():

? ? ????""" 其實(shí)是同步執(zhí)行 """
????????await coro_a()
????????await coro_b()

def show_perf(func):
????????start = time.perf_counter()
????????asyncio.run(func())
????????print(f'{func.__name__} Cost: {time.perf_counter() - start}')

>>> show_perf(sync_run) # 同步
>>>?Suspending coro_a
>>>?running coro_a
>>>?Suspending coro_b
>>>?running coro_b
>>>?ync_run Cost: 4.0023612

同步的原因解析:運(yùn)行?asyncio.run(func()) 后,將?func() 轉(zhuǎn)化成future對(duì)象,并放入事件循環(huán)中,隨后事件循環(huán)執(zhí)行的是sync_run() 里面的代碼,程序運(yùn)行至?await coro_a()處,暫停并阻塞,并將CPU控制權(quán)移交出去,直到coro_a() 執(zhí)行完成并重新獲取CPU控制權(quán)執(zhí)行下面的代碼coro_b(),一直如此,待整個(gè)程序結(jié)束, 所以這種和同步?jīng)]什么區(qū)別。
(1):await coro? 作如上解釋
(2):await task_or_future 不會(huì)阻塞

1.5.2 正確姿勢(shì)一:使用asyncio.gather 或?asyncio.wait

async def async_run():
? ??????await asyncio.gather(coro_a(), coro_b()) ???? # 異步
? ? ? ? # await asyncio.wait([coro_a(), coro_b()])? ? ? ? ?# 異步 Py3.11 will removal asyncio.wait

>>>?show_perf(async_run) # 異步
>>>?Suspending coro_a
>>>?Suspending coro_b
>>>?running coro_b
>>>?running coro_a
>>>?async_run Cost: 3.0031817

asyncio.gather

gather的參數(shù)為 coroutines_or_futures, 即協(xié)程 或 task 或 future 的可變參數(shù):
(1):?tasks =?await asyncio.gather(*[coro1, coro2, coro3])? |?asyncio.gather(coro1,?coro2,?coro3)
(2):? tasks = awaitasyncio.gather(*[task1, task2,task3])? |??asyncio.gather( task1,task2,task3)
(3):? tasks = awaitasyncio.gather(*[futu1, futu2,futu3])? |???asyncio.gather( futu1,?futu2,?futu3)
(4):? tasks = awaitasyncio.gather(*[coro1,?task2,?futu3])? |???asyncio.gather(?coro1?,??task2,?futu3)

返回的是已完成Task的result。

asyncio.wait

wait的參數(shù)為 fs, 即協(xié)程 或 task 或?future? 的單個(gè)列表或元祖:
(1):??tasks =??await?asyncio.wait([coro1,?coro2,?coro3])?
(2):??tasks =?await?asyncio.wait([task1, task2,task3])? ? ?
(3):? tasks =?await?asyncio.wait([futu1,?futu2,?futu3])? ? ?
(4):?tasks =?await?asyncio.wait([coro1,??task2,?futu3])? ?

async.wait會(huì)返回兩個(gè)值:done和pending,done為已完成的協(xié)程Task,pending為超時(shí)未完成的協(xié)程Task,需通過(guò)future.result調(diào)用Task的result。

1.5.3 正確姿勢(shì)二:await task_or_future

async def async_run():
? ??????# 異步
? ??????task_a = asyncio.create_task(coro_a())
????????task_b = asyncio.create_task(coro_b())
????????await task_a
????????await task_b

>>>?show_perf(async_run) # 異步OK
>>>?Suspending coro_a
>>>?Suspending coro_b
>>>??running coro_b
>>>?running coro_aasync_run
>>>?Cost: 3.0027209000000004

1.5.4 錯(cuò)誤姿勢(shì)三:await task_or_future,?直接await task不會(huì)對(duì)并發(fā)有幫助

async def async_run():
? ??????asyncio.create_task(coro_a())
? ??????asyncio.create_task(coro_b())

>>>?show_perf(async_run) # 同步
>>>?Suspending coro_a
>>>?running coro_a
>>>?Suspending coro_b
>>>?running coro_basync_run
>>>?Cost: 4.0028486

1.5.5 正確姿勢(shì)四:先 await coro, 再 await? task_or_future

async def async_run():
? ???????# 也可以用:asyncio.ensure_future(coro_a()) 或者?loop.create_task(coro_a())
? ??????task_a= asyncio.create_task(coro_a())? ? ? ? ?#??py3.7推薦用法
? ??????await coro_b()? ? ? ? # 先協(xié)程
? ??????await task_a? ? ? ? ? ? # 再task

>>>?show_perf(async_run)????? # 異步OK
>>>?Suspending coro_b
>>>?Suspending coro_a
>>>?running coro_b
>>>?running coro_aasync_run
>>>?Cost: 3.0015209

======================錯(cuò)誤姿勢(shì)=======================
? async def async_run():? ??????
? ??????# 也可以用:asyncio.ensure_future(coro_a()) 或者?loop.create_task(coro_a())
????????task_a?= asyncio.create_task(coro_a())?
????????await task_a? ? ? ? ? ? # ?先task?
????????await coro_b()? ? ? ? # 再協(xié)程? ? ? ??

>>>?show_perf(async_run)? ? ? # 同步
>>>?async_run Cost: 4.004120800000001
======================?錯(cuò)誤姿勢(shì)?=======================

疑問(wèn):正常來(lái)說(shuō)一般 await coro_task_or_future 程序會(huì)掛起,將cpu控制權(quán)移交給其他程序,但是在一個(gè)協(xié)程里多次直接 await有次序之分,那么對(duì)于 await 后程序掛起的理論就不能完全按上文提到的那樣理解,await究其機(jī)制到此我還并未真正探究明白,等后面補(bǔ)上,暫時(shí)只要能區(qū)分1.5.1 、 1.5.3、1.5.4、1.5.5 中各種姿勢(shì)的對(duì)錯(cuò)與否即可

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容