1. python yield與async/await
要點(diǎn)
最重要的是生成器函數(shù)碰到y(tǒng)ield停止執(zhí)行毫别,收到next或send才會(huì)繼續(xù)執(zhí)行的機(jī)制熄捍。
而且send方法令我們可以傳遞值到生成器暫停的地方。
生成器執(zhí)行結(jié)束拋出StopIteration
異常廓潜。
yield from用于把其他生成器當(dāng)做子例程調(diào)用抵皱。
MUST: yield from + iterable
MUST: @asyncio.coroutines + yield from / yield
MUST: await + coroutine / object with await() method (that must ruturn a iterable instead of a coroutine)
MUSTN'T: async def + yield/yield from
圖說
- 以下兩張圖中是6組代碼,實(shí)現(xiàn)了同樣的控制程序執(zhí)行順序的功能辩蛋,調(diào)度器一樣呻畸,輸出也完全一樣,幾個(gè)小的演變一定程度上展示了yield和async/await的關(guān)系悼院。
-
下面這張圖是用python內(nèi)部的審查函數(shù)來判斷類型伤为,查看上圖中的@coroutine+yield構(gòu)成的函數(shù)到底在python內(nèi)部被當(dāng)做什么
2. async/await 實(shí)驗(yàn)
- 這是一系列關(guān)于await,async特性的小實(shí)驗(yàn)樱蛤,該新特性在PEP-0492中描述并在Python3.5中實(shí)現(xiàn)钮呀。是一個(gè)相當(dāng)不錯(cuò)的便于初學(xué)者理解await剑鞍,async的資源昨凡。
- 在這里我翻譯了作者的綱要并闡述了自己的理解,不當(dāng)之處歡迎指正蚁署。
- 代碼參看github/awaitexp便脊。
實(shí)驗(yàn)一:最簡調(diào)度器
目的是有一個(gè)基本上最簡單可實(shí)現(xiàn)的協(xié)程調(diào)度器。它與標(biāo)準(zhǔn)庫中asyncio模塊中相對(duì)復(fù)雜的調(diào)度器并列光戈。
首先創(chuàng)建一個(gè)最簡單的基于生成器的協(xié)程函數(shù)A
@types.coroutine
def switch():
yield
然后它可以被其他用async def
定義的的協(xié)程函數(shù)B和C await
哪痰,只有當(dāng)await
返回時(shí),B和C才繼續(xù)執(zhí)行久妆。
這樣我們就可以有效地控制B和C的執(zhí)行順序晌杰。
然后我們創(chuàng)建了一個(gè)調(diào)度器,它對(duì)列表進(jìn)行了兩次深拷貝以避免問題筷弦。它循環(huán)協(xié)程隊(duì)列肋演,使用send
方法對(duì)每個(gè)協(xié)程依次遞進(jìn)抑诸,如果有協(xié)程已經(jīng)完成則將其移出隊(duì)列,當(dāng)列表中的協(xié)程全部完成時(shí)結(jié)束爹殊。
實(shí)驗(yàn)二:具有睡眠功能的簡單調(diào)度器
目的是給我們的調(diào)度器添加一個(gè)
awaitable
的睡眠協(xié)程函數(shù)蜕乡。盡管睡眠功能本身是相當(dāng)簡單的,這還是給調(diào)度器增加了一定的復(fù)雜度梗夸。之后該調(diào)度器的睡眠功能是否能簡易地與其他事件組合將是一個(gè)有趣的問題层玲。我們?cè)诘谝粋€(gè)實(shí)驗(yàn)的基礎(chǔ)上首先修改switch函數(shù)使其yield返回輸入給它的參數(shù)的字典,并且添加一個(gè)新的調(diào)用它的協(xié)程函數(shù)反症。
async def sleep(delay):
await switch(delay=delay)
然后通過args=coro.send(None)
與該函數(shù)碰撞辛块,得到含有delay
參數(shù)的字典作為send
的返回值。便可以判斷出是否調(diào)用調(diào)度器的睡眠機(jī)制铅碍。
最后在調(diào)度器中實(shí)現(xiàn)每一次協(xié)程列表循環(huán)結(jié)束后判斷在睡眠列表中的協(xié)程是否有到時(shí)間的憨降,到時(shí)間或時(shí)間超出則添加到運(yùn)行協(xié)程列表中進(jìn)入循環(huán)執(zhí)行。如果運(yùn)行列表中的協(xié)程都執(zhí)行完了该酗,則查看睡眠列表中的協(xié)程中還需睡眠的最少時(shí)間授药,線程睡眠,睡眠完成再將其添加到運(yùn)行隊(duì)列呜魄。
實(shí)驗(yàn)三:使能增加新的協(xié)程
當(dāng)前只有在開始指定的協(xié)程能被調(diào)度悔叽。該實(shí)驗(yàn)的目的是允許新的協(xié)程能夠在調(diào)度器開始運(yùn)行后被加入。
在當(dāng)前實(shí)驗(yàn)中我們拓展調(diào)度器為一個(gè)類并持有自己的協(xié)程隊(duì)列爵嗅,并提供一個(gè)添加協(xié)程進(jìn)入隊(duì)列的方法娇澎。然后獲取調(diào)度器的唯一實(shí)例,在定義的協(xié)程中使用實(shí)例方法來加入新的協(xié)程睹晒,再把該協(xié)程加入調(diào)度趟庄,這樣就實(shí)現(xiàn)了在運(yùn)行中的某一刻加入新的協(xié)程。
實(shí)驗(yàn)四:和線程池進(jìn)行交互
有時(shí)我們會(huì)有一些計(jì)算(或者I/O)任務(wù)伪很,它們最好能在背景中執(zhí)行戚啥,而不是在主線程中。這個(gè)實(shí)驗(yàn)展示了一個(gè)示例锉试,通過線程池執(zhí)行器和被調(diào)度的協(xié)程交互來獲得背景計(jì)算猫十。從長遠(yuǎn)看,這個(gè)技術(shù)很可能不是一個(gè)良好的基礎(chǔ)呆盖,因?yàn)樗荒芡瑫r(shí)伺服IO選擇器和線程隊(duì)列拖云。
在這一歩實(shí)驗(yàn)中我們主要是添加了兩個(gè)部分,第一部分是一個(gè)裝飾器:
def background(fn):
@wraps(fn)
async def wrapper(*args,**kwargs):
return await switch(op='background',fn=fn,args=args,kwargs=kwargs)
return wrapper
該裝飾器能將一個(gè)比較耗時(shí)的計(jì)算函數(shù)封裝為一個(gè)協(xié)程应又,使其可以被其他協(xié)程await
宙项。在調(diào)度器中利用send
函數(shù)的返回值可以獲取它的類型為background
、函數(shù)入口地址以及函數(shù)的傳參株扛,然后在調(diào)度器中按相應(yīng)機(jī)制執(zhí)行尤筐。
第二部分是在調(diào)度器中的修改:我們讓調(diào)度器類擁有了一個(gè)私有的concurrent.futures.ThreadPoolExecutor()
對(duì)象邑贴。并在運(yùn)行協(xié)程隊(duì)列的循環(huán)判斷中將background
類型的操作提交給線程池對(duì)象,并將當(dāng)前的協(xié)程移出運(yùn)行隊(duì)列叔磷,添加到futures
隊(duì)列中拢驾。然后在每次運(yùn)行隊(duì)列循環(huán)后判斷futures
中的任務(wù)是否有完成的(使用的參數(shù)為一旦有任一任務(wù)完成或被取消都返回),如果主線程此時(shí)處于將要睡眠的狀態(tài)改基,就等待相應(yīng)的時(shí)間繁疤,沒有的話則立刻返回,下次再查詢秕狰,完成的任務(wù)將其所在協(xié)程帶入運(yùn)行隊(duì)列稠腊,任務(wù)結(jié)果通過調(diào)度器send
傳回該協(xié)程。
實(shí)驗(yàn)五:
協(xié)程的一個(gè)典型應(yīng)用就是和異步I/O交互鸣哀。該實(shí)驗(yàn)的目的是讓調(diào)度器和Python的selector模型進(jìn)行交互并提供一個(gè)基本的I/O模型架忌。
類似于實(shí)驗(yàn)四,我們?cè)趯?shí)驗(yàn)三的基礎(chǔ)上讓調(diào)度器擁有了一個(gè)私有的
selectors.DefaultSelector()
對(duì)象我衬。創(chuàng)建了一個(gè)名為io
輔助協(xié)程叹放,供其他協(xié)程調(diào)用并且給調(diào)度器提供類型和操作信息。同樣利用主線程睡眠時(shí)間來等待selector
的select
方法挠羔。關(guān)鍵語句:for key, events in self.selector.select(timeout=timeout): ...
井仰,select
返回在超時(shí)時(shí)間內(nèi)IO準(zhǔn)備好的對(duì)象列表。
此外一部分是創(chuàng)建服務(wù)端和客戶端的協(xié)程破加。
在服務(wù)端的server
協(xié)程函數(shù)中首先初始化socket
俱恶,然后在主循環(huán)中await
io操作(服務(wù)器本身是一個(gè)協(xié)程!)范舀,利用io
輔助協(xié)程來等待io資源合是,取得連接后調(diào)用echo
函數(shù)處理該連接,并將其加入調(diào)度器锭环。在echo
協(xié)程中聪全,同樣利用io
輔助協(xié)程異步獲取讀寫權(quán)限。循環(huán)直到break結(jié)束關(guān)閉鏈接田藐。
對(duì)于客戶端測(cè)試程序荔烧,需要另外獲取一個(gè)調(diào)度器。定義一個(gè)echoclient
協(xié)程函數(shù)汽久,利用io
輔助協(xié)程異步發(fā)送接收信息。最后將所有測(cè)試協(xié)程加入調(diào)度器列表踊餐,開始執(zhí)行景醇。
實(shí)驗(yàn)六:
目的是有效地合并實(shí)驗(yàn)四和實(shí)驗(yàn)五的特性吝岭。這將使用標(biāo)準(zhǔn)的
self-pipe
(實(shí)際上是一個(gè)self-socket
)技巧來通知背景中復(fù)雜計(jì)算方法的完成三痰。在服務(wù)端的
echo
協(xié)程函數(shù)中添加了await add(3, 5)
語句吧寺,add
函數(shù)中添加sleep
代表耗時(shí)的背景計(jì)算。在調(diào)度器類中添加了自己的一對(duì)socket
散劫。當(dāng)有復(fù)雜計(jì)算時(shí)稚机,添加入futures
隊(duì)列,同時(shí)設(shè)置future
完成回調(diào)函數(shù)获搏,該回調(diào)函數(shù)向自己的socket
發(fā)送提示完成的消息赖条。在select
方法時(shí)判斷是否有準(zhǔn)備好的io描述符屬于自己擁有的socket
,如果有就意味著一個(gè)計(jì)算的完成常熙,然后清空socket緩存纬乍,移除相應(yīng)的futures
。
相關(guān)知識(shí)點(diǎn)
- selectors – High-level I/O multiplexing 基于select模塊原語
- file object - File object指一個(gè)對(duì)于底層資源暴露了面向文件API(擁有read和write方法)的對(duì)象裸卫。一個(gè)文件對(duì)象會(huì)被調(diào)制到訪問一個(gè)真實(shí)的磁盤文件或者其他類型的存儲(chǔ)或通信設(shè)備仿贬。它也被叫做類文件對(duì)象或者流。實(shí)際上有三類文件對(duì)象:原生二進(jìn)制文件墓贿,緩沖二進(jìn)制文件以及文本文件茧泪。它們的接口被定義在io模塊中。創(chuàng)建一個(gè)文件對(duì)象的典型方法是使用open方法聋袋。