Python 非同步 IO系列:認識asyncio

王平發表於2019-01-25

Python的asyncio是使用 async/await 語法編寫併發程式碼的標準庫。透過上一篇文章,我們瞭解了它不斷變化的發展歷史。到了Python最新穩定版 3.7 這個版本,asyncio又做了比較大的調整,把這個庫的API分為了 高層級API和低層級API,並引入asyncio.run()這樣的高階方法,讓編寫非同步程式更加簡潔。

introduce asyncio

本文希望提綱挈領地介紹最新 3.7 版的asnycio,先從全域性認識Python這個非同步IO庫。

asyncio的高層級API主要提高如下幾個方面:
– 併發地執行Python協程並完全控制其執行過程;
– 執行網路IO和IPC;
– 控制子程式;
– 透過佇列實現分散式任務;
– 同步併發程式碼。

asyncio的低層級API用以支援開發非同步庫和框架:
– 建立和管理事件迴圈(event loop),提供非同步的API用於網路,執行子程式,處理作業系統訊號等;
– 透過transports實現高效率協議;
– 透過async/await 語法橋架基於回撥的庫和程式碼。

asyncio高階API

高層級API讓我們更方便的編寫基於asyncio的應用程式。這些API包括:
(1)協程和任務
協程透過 async/await 語法進行宣告,是編寫非同步應用的推薦方式。歷史的 @asyncio.coroutine 和 yield from 已經被棄用,並計劃在Python 3.10中移除。協程可以透過 asyncio.run(coro, *, debug=False) 函式執行,該函式負責管理事件迴圈並完結非同步生成器。它應該被用作asyncio程式的主入口點,相當於main函式,應該只被呼叫一次。

任務被用於併發排程協程,可用於網路爬蟲的併發。使用 asyncio.create_task() 就可以把一個協程打包為一個任務,該協程會自動安排為很快執行。

協程,任務和Future都是可等待物件。其中,Future是低層級的可等待物件,表示一個非同步操作的最終結果。

(2)流
流是用於網路連線的高層級的使用 async/await的原語。流允許在不使用回撥或低層級協議和傳輸的情況下傳送和接收資料。非同步讀寫TCP有客戶端函式 asyncio.open_connection() 和 服務端函式 asyncio.start_server() 。它還支援 Unix Sockets: asyncio.open_unix_connection() 和 asyncio.start_unix_server()。

(3)同步原語
asyncio同步原語的設計類似於threading模組的原語,有兩個重要的注意事項:
asyncio原語不是執行緒安全的,因此它們不應該用於OS執行緒同步(而是用threading)
這些同步原語的方法不接受超時引數; 使用asyncio.wait_for()函式執行超時操作。
asyncio具有以下基本同步原語:
Lock
Event
Condition
Semaphore
BoundedSemaphore

(4)子程式
asyncio提供了透過 async/await 建立和管理子程式的API。不同於Python標準庫的subprocess,asyncio的子程式函式都是非同步的,並且提供了多種工具來處理這些函式,這就很容易並行執行和監視多個子程式。建立子程式的方法主要有兩個:

coroutine asyncio.create_subprocess_exec()
coroutine asyncio.create_subprocess_shell()

(5)佇列
asyncio 佇列的設計類似於標準模組queue的類。雖然asyncio佇列不是執行緒安全的,但它們被設計為專門用於 async/await 程式碼。需要注意的是,asyncio佇列的方法沒有超時引數,使用 asyncio.wait_for()函式進行超時的佇列操作。
因為和標註模組queue的類設計相似,使用起來跟queue無太多差異,只需要在對應的函式前面加 await 即可。asyncio 佇列提供了三種不同的佇列:
class asyncio.Queue 先進先出佇列
class asyncio.PriorityQueue 優先佇列
class asyncio.LifoQueue 後進先出佇列

(6)異常
asyncio提供了幾種異常,它們是:
TimeoutError,
CancelledError,
InvalidStateError,
SendfileNotAvailableError
IncompleteReadError
LimitOverrunError

asyncio低階API

低層級API為編寫基於asyncio的庫和框架提供支援,有意編寫非同步庫和框架的大牛們需要熟悉這些低層級API。主要包括:

(1)事件迴圈
事件迴圈是每個asyncio應用程式的核心。 事件迴圈執行非同步任務和回撥,執行網路IO操作以及執行子程式。

應用程式開發人員通常應該使用高階asyncio函式,例如asyncio.run(),並且很少需要引用迴圈物件或呼叫其方法。

Python 3.7 新增了 asyncio.get_running_loop()函式。

(2)Futures
Future物件用於將基於低層級回撥的程式碼與高層級的 async/await 程式碼進行橋接。
Future表示非同步操作的最終結果。 不是執行緒安全的。
Future是一個可等待物件。 協程可以等待Future物件,直到它們有結果或異常集,或者直到它們被取消。
通常,Futures用於啟用基於低層級回撥的程式碼(例如,在使用asyncio傳輸實現的協議中)以與高層級 async/await 程式碼進行互操作。

(3)傳輸和協議(Transports和Protocols)
Transport 和 Protocol由低層級事件迴圈使用,比如函式loop.create_connection()。它們使用基於回撥的程式設計風格,並支援網路或IPC協議(如HTTP)的高效能實現。

在最高階別,傳輸涉及位元組的傳輸方式,而協議確定要傳輸哪些位元組(在某種程度上何時傳輸)。

換種方式說就是:傳輸是套接字(或類似的I/O端點)的抽象,而協議是從傳輸的角度來看的應用程式的抽象。

另一種觀點是傳輸和協議介面共同定義了一個使用網路I/O和程式間I/O的抽象介面。

傳輸和協議物件之間始終存在1:1的關係:協議呼叫傳輸方法來傳送資料,而傳輸呼叫協議方法來傳遞已接收的資料。

大多數面向連線的事件迴圈方法(例如loop.create_connection())通常接受protocol_factory引數,該引數用於為接受的連線建立Protocol物件,由Transport物件表示。 這些方法通常返回(傳輸,協議)元組。

(4)策略(Policy)
事件迴圈策略是一個全域性的按程式劃分的物件,用於控制事件迴圈的管理。 每個事件迴圈都有一個預設策略,可以使用策略API對其進行更改和自定義。

策略定義了上下文的概念,並根據上下文管理單獨的事件迴圈。 預設策略將上下文定義為當前執行緒。

透過使用自定義事件迴圈策略,可以自定義get_event_loop(),set_event_loop()和new_event_loop()函式的行為。

(5)平臺支援
asyncio模組設計為可移植的,但由於平臺的底層架構和功能,某些平臺存在細微的差異和限制。在Windows平臺,有些是不支援的,比如 loop.create_unix_connection() and loop.create_unix_server()。而Linux和比較新的macOS全部支援。

結語

Python 3.7 透過對asyncio分組使得它的架構更加清晰,普通寫非同步IO的應用程式只需熟悉高層級API,需要寫非同步IO的庫和框架時才需要理解低層級的API。

後面,我們將結合具體例項來學習asyncio的使用。如果你有好的建議和意見,歡迎留言與我們討論。

擴充套件閱讀:
關於非同步IO的概念和歷史

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章