前言
- 這幾天看asyncio相關的pycon視頻又重溫了asyncio 的官方文檔,收獲很多。之前asyncio被吐槽的一點就是文檔寫的不好,Python 3.7 時 asyncio 的官方文檔被 Andrew Svetlov 以及 Yury Selivanov 等核心開發者重寫了,新的版本我覺得已經好很多了。借著這篇筆記記錄一下我對asyncio的一些理解。
核心概念
asyncio里面主要有4個需要關注的基本概念
Eventloop
Eventloop可以說是asyncio應用的核心,是中央總控。Eventloop實例提供了注冊、取消和執行任務和回調的方法。
把一些異步函數(就是任務,Task,一會就會說到)注冊到這個事件循環上,事件循環會循環執行這些函數(但同時只能執行一個),當執行到某個函數時,如果它正在等待I/O返回,事件循環會暫停它的執行去執行其他的函數;當某個函數完成I/O后會恢復,下次循環到它的時候繼續執行。因此,這些異步函數可以協同(Cooperative)運行:這就是事件循環的目標
Coroutine
-
協程(Coroutine)本質上是一個函數,特點是在代碼塊中可以將執行權交給其他協程:
在這里插入圖片描述
這里面有4個重要關鍵點:
協程要用 asyncdef聲明,Python 3.5時的裝飾器寫法已經過時,我就不列出來了。
asyncio.gather用來并發運行任務,在這里表示協同的執行a和b2個協程
在協程a中,有一句 awaitasyncio.sleep(0),await表示調用協程,sleep 0并不會真的sleep(因為時間為0),但是卻可以把控制權交出去了。
asyncio.run是Python 3.7新加的接口,要不然你得這么寫:
-
在這里插入圖片描述
在這里插入圖片描述
看到了吧,在并發執行中,協程a被掛起又恢復過。
Future
接著說Future,它代表了一個「未來」對象,異步操作結束后會把最終結果設置到這個Future對象上。Future是對協程的封裝,不過日常開發基本是不需要直接用這個底層Future類的。我在這里只是演示一下:
可以對這個Future實例添加完成后的回調(add_done_callback)、取消任務(cancel)、設置最終結果(set_result)、設置異常(如果有的話,set_exception)等。現在我們讓Future完成:
看到了吧,await之后狀態成了finished。這里順便說一下,一個對象怎么樣就可以被await(或者說怎么樣就成了一個awaitable對象)呢?給類實現一個await方法,Python版本的Future的實現大概如下:
這樣就可以 awaitfuture了,那為什么 awaitfuture后Future的狀態就能改變呢,這是因為用 loop.run_in_executor創建的Future注冊了一個回調(通過 asyncio.futures.wrap_future,加了一個 _call_set_state回調, 有興趣的可以通過延伸閱讀鏈接2找上下文)。
await里面的 yieldself不要奇怪,主要是為了兼容 iter,給舊的 yieldfrom用:
Task
Eventloop除了支持協程,還支持注冊Future和Task2種類型的對象,那為什么要存在Future和Task這2種類型呢?
先回憶前面的例子,Future是協程的封裝,Future對象提供了很多任務方法(如完成后的回調、取消、設置任務結果等等),但是開發者并不需要直接操作Future這種底層對象,而是用Future的子類Task協同的調度協程以實現并發。
Task非常容易創建和使用:
asyncio并發的正確/錯誤姿勢
在代碼中使用async/await是不是就能發揮asyncio的并發優勢么,其實是不對的,我們先看個例子:
有2個協程a和b,分別sleep1秒和3秒,如果協程可以并發執行,那么執行時間應該是sleep最大的那個值(3秒),現在它們都在s1協程里面被調用。大家先猜一下s1會運行幾秒?
我們寫個小程序驗證一下:
大家注意我這個時間計數用的方法,沒有用time.time,而是用了Python 3.3新增的time.perf_counter它是現在推薦的用法。我們在IPython里面驗證下:
看到了吧,4秒!!!,相當于串行的執行了(sleep 3 + 1)。這是錯誤的用法,應該怎么用呢,前面的asyncio.gather就可以:
看到了吧,3秒!另外一個是asyncio.wait:
同樣是3秒。先別著急,gather和wait下篇文章還會繼續對比。還有一個方案就是用asyncio.create_task
都是3秒。asyncio.create_task相當于把協程封裝成Task。不過大家要注意一個錯誤的用法:
直接await task不會對并發有幫助*。asyncio.createtask是Python 3.7新增的高階API,是推薦的用法,其實你還可以用asyncio.ensure_future和loop.createtask:
到這里,我們一共看到2種錯誤的,6種正確的寫法。你學到了么?