非常適合小白的 Asyncio 教程

2021-02-21 Python極客專欄

所謂「異步 IO」,就是你發起一個 IO 操作,卻不用等它結束,你可以繼續做其他事情,當它結束時,你會得到通知。

Asyncio 是並發(concurrency)的一種方式。對 Python 來說,並發還可以通過線程(threading)和多進程(multiprocessing)來實現。

Asyncio 並不能帶來真正的並行(parallelism)。當然,因為 GIL(全局解釋器鎖)的存在,Python 的多線程也不能帶來真正的並行。

可交給 asyncio 執行的任務,稱為協程(coroutine)。一個協程可以放棄執行,把機會讓給其它協程(即 yield from 或 await)。

1. 定義協程

協程的定義,需要使用 async def 語句。

async def do_some_work(x): pass

do_some_work 便是一個協程。
準確來說,do_some_work 是一個協程函數,可以通過 asyncio.iscoroutinefunction 來驗證:

print(asyncio.iscoroutinefunction(do_some_work)) # True

這個協程什麼都沒做,我們讓它睡眠幾秒,以模擬實際的工作量 :

async def do_some_work(x):
    print("Waiting " + str(x))
    await asyncio.sleep(x)

在解釋 await 之前,有必要說明一下協程可以做哪些事。協程可以:

等待一個 future 結束

等待另一個協程(產生一個結果,或引發一個異常)

產生一個結果給正在等它的協程

引發一個異常給正在等它的協程

asyncio.sleep 也是一個協程,所以 await asyncio.sleep(x) 就是等待另一個協程。可參見 asyncio.sleep 的文檔:

sleep(delay, result=None, *, loop=None)
Coroutine that completes after a given time (in seconds).

2. 運行協程

調用協程函數,協程並不會開始運行,只是返回一個協程對象,可以通過 asyncio.iscoroutine 來驗證:

print(asyncio.iscoroutine(do_some_work(3))) # True

此處還會引發一條警告:

async1.py:16: RuntimeWarning: coroutine 'do_some_work' was never awaited
  print(asyncio.iscoroutine(do_some_work(3)))

要讓這個協程對象運行的話,有兩種方式:

簡單來說,只有 loop 運行了,協程才可能運行。
下面先拿到當前線程預設的 loop ,然後把協程對象交給 loop.run_until_complete,協程對象隨後會在 loop 裡得到運行。

loop = asyncio.get_event_loop()
loop.run_until_complete(do_some_work(3))

run_until_complete 是一個阻塞(blocking)調用,直到協程運行結束,它才返回。這一點從函數名不難看出。
run_until_complete 的參數是一個 future,但是我們這裡傳給它的卻是協程對象,之所以能這樣,是因為它在內部做了檢查,通過 ensure_future 函數把協程對象包裝(wrap)成了 future。所以,我們可以寫得更明顯一些:

loop.run_until_complete(asyncio.ensure_future(do_some_work(3)))

完整代碼:

import asyncio

async def do_some_work(x):
    print("Waiting " + str(x))
    await asyncio.sleep(x)

loop = asyncio.get_event_loop()
loop.run_until_complete(do_some_work(3))

運行結果:

Waiting 3
<三秒鐘後程序結束>

3. 回調函數

假如協程是一個 IO 的讀操作,等它讀完數據後,我們希望得到通知,以便下一步數據的處理。這一需求可以通過往 future 添加回調來實現。

def done_callback(futu):
    print('Done')

futu = asyncio.ensure_future(do_some_work(3))
futu.add_done_callback(done_callback)

loop.run_until_complete(futu)

4. 多個協程

實際項目中,往往有多個協程,同時在一個 loop 裡運行。為了把多個協程交給 loop,需要藉助 asyncio.gather 函數。

loop.run_until_complete(asyncio.gather(do_some_work(1), do_some_work(3)))

或者先把協程存在列表裡:

coros = [do_some_work(1), do_some_work(3)]
loop.run_until_complete(asyncio.gather(*coros))

運行結果:

Waiting 3
Waiting 1
<等待三秒鐘>
Done

這兩個協程是並發運行的,所以等待的時間不是 1 + 3 = 4 秒,而是以耗時較長的那個協程為準。

參考函數 gather 的文檔:

gather(*coros_or_futures, loop=None, return_exceptions=False)
Return a future aggregating results from the given coroutines or futures.

發現也可以傳 futures 給它:

futus = [asyncio.ensure_future(do_some_work(1)),
             asyncio.ensure_future(do_some_work(3))]

loop.run_until_complete(asyncio.gather(*futus))

gather 起聚合的作用,把多個 futures 包裝成單個 future,因為 loop.run_until_complete 只接受單個 future。

5. run_until_complete和run_forever

我們一直通過 run_until_complete 來運行 loop ,等到 future 完成,run_until_complete 也就返回了。

async def do_some_work(x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

loop = asyncio.get_event_loop()

coro = do_some_work(3)
loop.run_until_complete(coro)

輸出:

Waiting 3
<等待三秒鐘>
Done
<程序退出>

現在改用 run_forever:

async def do_some_work(x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

loop = asyncio.get_event_loop()

coro = do_some_work(3)
asyncio.ensure_future(coro)

loop.run_forever()

輸出:

Waiting 3
<等待三秒鐘>
Done
<程序沒有退出>

三秒鐘過後,future 結束,但是程序並不會退出。run_forever 會一直運行,直到 stop 被調用,但是你不能像下面這樣調 stop:

loop.run_forever()
loop.stop()

run_forever 不返回,stop 永遠也不會被調用。所以,只能在協程中調 stop:

async def do_some_work(loop, x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')
    loop.stop()

這樣並非沒有問題,假如有多個協程在 loop 裡運行:

asyncio.ensure_future(do_some_work(loop, 1))
asyncio.ensure_future(do_some_work(loop, 3))

loop.run_forever()

第二個協程沒結束,loop 就停止了——被先結束的那個協程給停掉的。
要解決這個問題,可以用 gather 把多個協程合併成一個 future,並添加回調,然後在回調裡再去停止 loop。

async def do_some_work(loop, x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

def done_callback(loop, futu):
    loop.stop()

loop = asyncio.get_event_loop()

futus = asyncio.gather(do_some_work(loop, 1), do_some_work(loop, 3))
futus.add_done_callback(functools.partial(done_callback, loop))

loop.run_forever()

其實這基本上就是 run_until_complete 的實現了,run_until_complete 在內部也是調用 run_forever。

6. Close Loop?

以上示例都沒有調用 loop.close,好像也沒有什麼問題。所以到底要不要調 loop.close 呢?
簡單來說,loop 只要不關閉,就還可以再運行。:

loop.run_until_complete(do_some_work(loop, 1))
loop.run_until_complete(do_some_work(loop, 3))
loop.close()

但是如果關閉了,就不能再運行了:

loop.run_until_complete(do_some_work(loop, 1))
loop.close()
loop.run_until_complete(do_some_work(loop, 3))  # 此處異常

建議調用 loop.close,以徹底清理 loop 對象防止誤用。

7. gather 和 wait

asyncio.gather 和 asyncio.wait 功能相似。

coros = [do_some_work(loop, 1), do_some_work(loop, 3)]
loop.run_until_complete(asyncio.wait(coros))

具體差別可請參見 StackOverflow 的討論:Asyncio.gather vs asyncio.wait。

8. Timer

C++ Boost.Asio 提供了 IO 對象 timer,但是 Python 並沒有原生支持 timer,不過可以用 asyncio.sleep 模擬。

async def timer(x, cb):
    futu = asyncio.ensure_future(asyncio.sleep(x))
    futu.add_done_callback(cb)
    await futu

t = timer(3, lambda futu: print('Done'))
loop.run_until_complete(t)

相關焦點

  • Python | 非常適合小白的 Asyncio 教程
    協程可以:* 等待一個 future 結束* 等待另一個協程(產生一個結果,或引發一個異常)* 產生一個結果給正在等它的協程* 引發一個異常給正在等它的協程asyncio.sleep 也是一個協程,所以 await asyncio.sleep(x) 就是等待另一個協程。
  • Python裡最難的Asyncio,這裡有一份非常適合小白的教程
    協程可以:等待一個 future 結束等待另一個協程(產生一個結果,或引發一個異常)產生一個結果給正在等它的協程引發一個異常給正在等它的協程asyncio.sleep 也是一個協程,所以 await asyncio.sleep(x) 就是等待另一個協程。
  • Python 協程模塊 asyncio 使用指南
    我們用到是 Python 標準庫的 asyncio 模塊。asyncio 是異步教程中必須學習的基礎模塊。協程的好處就是單線程即可輕鬆實現百萬級的並發,同時速度上也得到大大提升,在爬蟲領域和 Web 開發領域等都有很大的用途,但是,這些模塊都是要配合協程模塊 asyncio 使用的。
  • python之asyncio包處理並發2
    #適合asyncio API 的協程在定義體中必須使用yield from,而不能使用yield。
  • python : 利用 asyncio 進行快速抓取
    我喜歡DIY的原因在於其靈活性,但是卻不適合用來做大量數據的抓取,因為需要請求同步,所以大量的請求意味著你不得不等待很長時間。在本文中,我將會為你展示一個基於新的異步庫(aiohttp)的請求的代替品。我使用它寫了一些速度的確很快的小數據抓取器,下面我將會為你演示是如何做到的。
  • Python中asyncio神器的入門
    asyncioasyncio是Python 3.4版本引入的標準庫,直接內置了對異步IO的支持。不僅有可以用作後端異步編程aiohttp,也可在爬蟲中發揮出異步的優勢的asyncio,甚至可以比Scrapy還要快,這種神器,其實也並不難理解。從Python 3.5開始引入了新的語法async和await,讓協程的代碼更簡潔易讀。
  • Python異步編程模塊asyncio學習
    asyncio是Python3.4引入的一個標準庫,直接內置了對異步IO的支持。asyncio模塊提供了使用協程構建並發應用的工具。它使用一種單線程單進程的的方式實現並發,應用的各個部分彼此合作, 可以顯示的切換任務,一般會在程序阻塞I/O操作的時候發生上下文切換如等待讀寫文件,或者請求網絡。
  • python異步編程模塊asyncio學習(二)
    在上次我們說如何使用asyncio,詳情點擊:python異步編程模塊asyncio學習(一)。接下來我們繼續學習關於異步模塊asyncio的其他操作。儘管asyncio應用通常作為單線程運行,不過仍被構建為並發應用。由於I/O以及其他外部事件的延遲和中斷,每個協程或任務可能按一種不可預知的順序執行。
  • 100集PS教程免費領:入門教學,適合零基礎小白
    本人花錢領取的ps入門教程,現免費贈送給大家,文末附免費領取方式。在這個美圖、美顏已成為必備技能,PS已成為自媒體素材製作不可缺少的工具的時代,如何快速入門並熟練應用PS技能成為很多小夥伴們的硬性需求,本次分享的教程,通過本人親身實踐,十分適合入門級小白學習,我作為一個工程人,以前對PS接觸非常少,更不用談應用並掌握了,但最近隨著開始做一下自媒體內容,逐漸嘗試學習PS,感覺這個教學視頻還是很不錯的,教程比較全面的講解了PS裡的各項工具並附有案例講解
  • 【翻譯】Awesome Asyncio 中文版
    譯者:  陳鍵冬     一個有趣的靈魂github:https://github.com/chenjiandongx校譯:呆鳥原文地址:https://github.com/timofurrer/awesome-asyncio
  • python之asyncio包處理並發5
    #改進asyncio下載腳本 展示如何使用100 個並發請求(-m 100)從ERROR 伺服器中下載100 面國旗(-al 100)。運行flags2_asyncio.py 腳本#重寫後的函數可以供concurrent.future 版重用。
  • 新手教程:立領休閒襯衣製作,附裁剪圖,簡單好做,適合小白練手
    大家好,今天給大家分享一款小立領休閒襯衣的製作教程,這個款式非常簡單好做,適合新手朋友練手,裁剪圖在文末根據裁剪圖裁剪出裁片,袖口,前領條,領子要粘襯的後中收皺後,後育克把後身夾在中間拼合1釐米縫份,圖中中間有褶皺的是大身
  • 《小白賺錢》app使用教程分享
    小白賺錢app是一款類似眾人幫的手賺軟體,軟體中有各類任務,玩家們每天打開應用即可做兼職,許多玩家不知道小白賺錢怎麼用,接下來小編就給大家帶來了app使用教程分享,快來一起看看吧! 小白賺錢app是一款類似眾人幫的手賺軟體,軟體中有各類任務,玩家們每天打開應用即可做兼職,許多玩家不知道小白賺錢怎麼用
  • 小白站長網站搭建教程
    今天阿呆給大家講一下如何搭建自己的個人網站適合小白站長的個人網站搭建在搭建網站之前需要準備的材料:1.一個備案過的域名【可以自己購買一個域名並備案一下,也可以上百度上百度免費二級域名在平臺上註冊一個免費的二級域名
  • FaceApp新手小白操作使用教程 FaceApp怎麼用?
    FaceApp新手小白操作使用教程 FaceApp怎麼用? 在最近國內外的社交網絡上非常流行的FaceApp想必很多人都下載嘗鮮了,不過有的人還不知道怎麼操作,下面就來為大家分享一下FaceApp新手小白操作使用教程。
  • 或許是最適合小白的Vlog神器 橙影攝像機體驗
    今天芒果給大家分享一款非常適合小白用的Vlog神器——橙影M1在選擇一款合適的Vlog設備之前,我們先對市場上常見的Vlog設備做個簡單的對比分析,微單畫質好,但是學習成本較高,機身龐大攜帶不便;手持穩定器拍攝穩定性較好,但拍攝全程佔用手機,無法回復消息,另外藍牙連接也相對麻煩些;對於大多數用戶而言,手機是使用最多的視頻拍攝工具,但手機防抖效果有限,不支持實時的智能追蹤,在一定程度上影響了出片;
  • 小白的自學Python線路
    開發工具pycharmPycharm是最好的Python代碼編輯工具之一,學習成本非常底,下載安裝後可直接上手。下載地址:https://jupyter.org/install自學網站菜鳥網站的這個python教程很適合小白學習,沒有高深的原理,照著教程學就完事了。
  • CG原畫插畫教程:萌新小白畫黃昏場景插畫教程
    藝學繪小編收集整理了萌新小白畫黃昏場景插畫教程 上底色這裡分析一下新手畫這種黃昏底色時常見的幾個誤區: 天色想和從頭高到尾,,明顯變化非常小,這種看出來效果大多是不ok的,多觀察一下觀實
  • 南方CASS教程+視頻講解+插件匯總,小白快速上手的測繪神器
    南方CASS教程+視頻講解+插件匯總,小白快速上手的測繪神器自CASS軟體推出以來,市場佔有率遙遙領先,已經成為業內應用最廣、使用最方便快捷的軟體品牌。也是用戶量最大、升級最快、服務最好的主流成圖和土石方計算軟體系統。
  • 適合小白的t恤衫畫法教程
    在很多我們所熟悉的漫畫和插畫中,T恤衫也是主角很常見的一種休閒服裝了,而且它的結構也不複雜,還適合加很多有個性的圖案。但是如何把T恤衫畫得有「穿上去」的感覺,其實還是非常有講究的。本教程就重點說說T恤衫的具體畫法。一、人物正面的T恤衫穿著1、多數T恤衫是比較寬鬆的,從正面來看尤其是在下擺位置,要有「飄起來」的感覺。在尤其是女生在胸口的位置,會因為胸部凸起而會產生一些大的褶皺。