asyncio 是 Python 中的異步IO庫藕届,用來編寫并發(fā)協(xié)程膝藕,適用于IO阻塞且需要大量并發(fā)的場景,例如爬蟲承冰、文件讀寫华弓。
asyncio 在 Python3.4 被引入,經(jīng)過幾個版本的迭代困乒,特性该抒、語法糖均有了不同程度的改進,這也使得不同版本的 Python 在 asyncio 的用法上各不相同顶燕,顯得有些雜亂凑保,以前使用的時候也是本著能用就行的原則,在寫法上走了一些彎路涌攻,現(xiàn)在對 Python3.7+ 和 Python3.6 中 asyncio 的用法做一個梳理欧引,以便以后能更好的使用。
1. 協(xié)程與asyncio
協(xié)程恳谎,又稱微線程芝此,它不被操作系統(tǒng)內(nèi)核所管理,而完全是由程序控制因痛,協(xié)程切換花銷小婚苹,因而有更高的性能。
協(xié)程可以比作子程序鸵膏,不同的是膊升,執(zhí)行過程中協(xié)程可以掛起當前狀態(tài),轉(zhuǎn)而執(zhí)行其他協(xié)程谭企,在適當?shù)臅r候返回來接著執(zhí)行廓译,協(xié)程間的切換不需要涉及任何系統(tǒng)調(diào)用或任何阻塞調(diào)用评肆,完全由協(xié)程調(diào)度器進行調(diào)度。
Python 中以 asyncio 為依賴非区,使用 async/await 語法進行協(xié)程的創(chuàng)建和使用瓜挽,如下 async 語法創(chuàng)建一個協(xié)程函數(shù):
async def work():
pass
在協(xié)程中除了普通函數(shù)的功能外最主要的作用就是:使用 await 語法等待另一個協(xié)程結(jié)束,這將掛起當前協(xié)程征绸,直到另一個協(xié)程產(chǎn)生結(jié)果再繼續(xù)執(zhí)行:
async def work():
await asyncio.sleep(1)
print('continue')
asyncio.sleep()
是 asyncio 包內(nèi)置的協(xié)程函數(shù)久橙,這里模擬耗時的IO操作,上面這個協(xié)程執(zhí)行到這一句會掛起當前協(xié)程而去執(zhí)行其他協(xié)程管怠,直到sleep結(jié)束淆衷,當有多個協(xié)程任務(wù)時,這種切換會讓它們的IO操作并行處理排惨。
注意,執(zhí)行一個協(xié)程函數(shù)并不會真正的運行它碰凶,而是會返回一個協(xié)程對象暮芭,要使協(xié)程真正的運行,需要將它們加入到事件循環(huán)中運行欲低,官方建議 asyncio 程序應(yīng)當有一個主入口協(xié)程辕宏,用來管理所有其他的協(xié)程任務(wù):
async def main():
await work()
在 Python3.7+ 中,運行這個 asyncio 程序只需要一句:asyncio.run(main())
砾莱,而在 Python3.6 中瑞筐,需要手動獲取事件循環(huán)并加入?yún)f(xié)程任務(wù):
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
事件循環(huán)就是一個循環(huán)隊列,對其中的協(xié)程進行調(diào)度執(zhí)行腊瑟,當把一個協(xié)程加入循環(huán)聚假,這個協(xié)程創(chuàng)建的其他協(xié)程都會自動加入到當前事件循環(huán)中。
其實協(xié)程對象也不是直接運行闰非,而是被封裝成一個個待執(zhí)行的 Task 膘格,大多數(shù)情況下 asyncio 會幫我們進行封裝,我們也可以提前自行封裝 Task 來獲得對協(xié)程更多的控制權(quán)财松,注意瘪贱,封裝 Task 需要 當前線程有正在運行的事件循環(huán),否則將引 RuntimeError辆毡,這也就是官方建議使用主入口協(xié)程的原因菜秦,如果在主入口協(xié)程之外創(chuàng)建任務(wù)就需要先手動獲取事件循環(huán)然后使用底層方法 loop.create_task()
,而在主入口協(xié)程之內(nèi)是一定有正在運行的循環(huán)的舶掖。任務(wù)創(chuàng)建后便有了狀態(tài)球昨,可以查看運行情況,查看結(jié)果眨攘,取消任務(wù)等:
async def main():
task = asyncio.create_task(work())
print(task)
await task
print(task)
#----執(zhí)行結(jié)果----#
<Task pending name='Task-2' coro=<work() running at d:\tmp\code\asy.py:5>>
<Task finished name='Task-2' coro=<work() done, defined at d:\tmp\code\asy.py:5> result=None>
asyncio.create_task()
是 Python3.7 加入的高層級API褪尝,在 Python3.6闹获,需要使用低層級API asyncio.ensure_future()
來創(chuàng)建 Future,F(xiàn)uture 也是一個管理協(xié)程運行狀態(tài)的對象河哑,與 Task 沒有本質(zhì)上的區(qū)別避诽。
2. 并發(fā)協(xié)程
通常,一個含有一系列并發(fā)協(xié)程的程序?qū)懛ㄈ缦拢≒ython3.7+):
import asyncio
import time
async def work(num: int):
'''
一個工作協(xié)程璃谨,接收一個數(shù)字沙庐,將它 +1 后返回
'''
print(f'working {num} ...')
await asyncio.sleep(1) # 模擬耗時的IO操作
print(f'{num} -> {num+1} done')
return num + 1
async def main():
'''
主協(xié)程,創(chuàng)建一系列并發(fā)協(xié)程并運行它們
'''
# 任務(wù)隊列
tasks = [work(num) for num in range(0, 5)]
# 并發(fā)執(zhí)行隊列中的協(xié)程并等待結(jié)果返回
results = await asyncio.gather(*tasks)
print(results)
if __name__ == "__main__":
asyncio.run(main())
并發(fā)運行多個協(xié)程任務(wù)的關(guān)鍵就是 asyncio.gather(*tasks)
佳吞,它接受多個協(xié)程任務(wù)并將它們加入到事件循環(huán)拱雏,所有任務(wù)都運行完成后會返回結(jié)果列表,這里我們也沒有手動封裝 Task底扳,因為 gather 函數(shù)會自動封裝铸抑。
并發(fā)運行還有另一個方法 asyncio.wait(tasks)
,它們的區(qū)別是:
- gather 比 wait 更加高層衷模,gather 可以將任務(wù)分組鹊汛,一般優(yōu)先使用 gather:
tasks1 = [work(num) for num in range(0, 5)]
tasks2 = [work(num) for num in range(5, 10)]
group1 = asyncio.gather(*tasks1)
group2 = asyncio.gather(*tasks2)
results1, results2 = await asyncio.gather(group1, group2)
print(results1, results2)
- 在某些定制化任務(wù)需求的時候,可以使用 wait:
# Python3.8 版本后阱冶,直接向 wait() 傳入?yún)f(xié)程對象已棄用刁憋,必須手動創(chuàng)建 Task
tasks = [asyncio.create_task(work(num)) for num in range(0, 5)]
done, pending = await asyncio.wait(tasks)
for task in tasks:
if task in done:
print(task.result())
for p in pending:
p.cancel()
3. Tips
- await 語句后必須是一個 可等待對象 ,可等待對象主要有三種:Python協(xié)程木蹬,Task至耻,F(xiàn)uture。通常情況下沒有必要在應(yīng)用層級的代碼中創(chuàng)建 Future 對象镊叁。
- 在 asyncio 程序中使用同步代碼雖然并不會報錯尘颓,但是也失去了并發(fā)的意義,例如網(wǎng)絡(luò)請求晦譬,如果使用僅支持同步的 requests泥耀,在發(fā)起一次請求后在收到響應(yīng)結(jié)果之前不能發(fā)起其他請求,這樣要并發(fā)訪問多個網(wǎng)頁時蛔添,即使使用了 asyncio痰催,在發(fā)送一次請求后切換到其他協(xié)程還是會因為同步問題而阻塞,并不能有速度上的提升迎瞧,這時候就需要其他支持異步操作的請求庫如 aiohttp夸溶。
- 關(guān)于 asyncio 的更多更詳細的操作見 官方文檔