Python協(xié)程之a(chǎn)syncio

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 的更多更詳細的操作見 官方文檔
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凶硅,隨后出現(xiàn)的幾起案子缝裁,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捷绑,死亡現(xiàn)場離奇詭異韩脑,居然都是意外死亡,警方通過查閱死者的電腦和手機粹污,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門段多,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人壮吩,你說我怎么就攤上這事进苍。” “怎么了鸭叙?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵觉啊,是天一觀的道長。 經(jīng)常有香客問我沈贝,道長杠人,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任宋下,我火速辦了婚禮嗡善,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杨凑。我一直安慰自己滤奈,他們只是感情好摆昧,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布撩满。 她就那樣靜靜地躺著,像睡著了一般绅你。 火紅的嫁衣襯著肌膚如雪伺帘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天忌锯,我揣著相機與錄音伪嫁,去河邊找鬼。 笑死偶垮,一個胖子當著我的面吹牛张咳,可吹牛的內(nèi)容都是我干的似舵。 我是一名探鬼主播脚猾,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼砚哗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蛛芥?” 一聲冷哼從身側(cè)響起提鸟,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤军援,失蹤者是張志新(化名)和其女友劉穎称勋,沒想到半個月后胸哥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铣缠,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡烘嘱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝗蛙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝇庭。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡捡硅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出壮韭,到底是詐尸還是另有隱情,我是刑警寧澤喷屋,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布屯曹,位于F島的核電站狱庇,受9級特大地震影響恶耽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偷俭,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淹遵。 院中可真熱鬧,春花似錦透揣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拆祈。三九已至,卻和暖如春咙咽,著一層夾襖步出監(jiān)牢的瞬間淤年,已是汗流浹背钧敞。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工麸粮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弄诲。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓齐遵,卻偏偏與公主長得像寂玲,于是被迫代替她去往敵國和親梗摇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容