異步框架之深入理解asyncio(一)

前言

  • 這幾天看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種正確的寫法。你學到了麼?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章