天天看點

python異步IO程式設計(一)

python異步IO程式設計(一)基礎概念

異步IO (async IO):一種由多種語言實作的與語言無關的範例(或模型)。

asyncio:Python 3.4版本引入的标準庫,直接内置了對異步IO的支援。

異步IO

多線程善于處理I/O密集型任務。

多程序擅長處理計算密集型(CPU-bound)任務:強密集循環和數學計算都屬于此類。

并發是并行的一種特殊類型(或者說子類),多線程是并發的表現形式,多程序是并行的表現形式。

Python通過它的包 multiprocessing,threading 和 concurrent.futures 已經對這兩種形式都提供了長期的支援。

異步IO是一種單程序、單線程的設計:它使用協同多任務處理機制,是以協程為核心的一種程式設計模型。

異步IO并不是新發明的概念,它已經存在或正在被建構到其他語言及運作時環境中,如 Go,C# 和 Scala 等。

異步IO模型異步IO采用消息循環的模式,在消息循環中,主線程不斷地重複“讀取消息-處理消息”這一過程:

由于GUI線程處理鍵盤、滑鼠等消息的速度非常快,是以使用者感覺不到延遲。某些時候,GUI線程在一個消息處理的過程中遇到問題導緻一次消息處理時間過長,此時,使用者會感覺到整個GUI程式停止響應了,敲鍵盤、點滑鼠都沒有反應。

這種情況說明在消息模型中,處理一個消息必須非常迅速,否則,主線程将無法及時處理消息隊列中的其他消息,導緻程式看上去停止響應。

消息模型是如何解決同步IO必須等待IO操作這一問題的呢?當遇到IO操作時,代碼隻負責發出IO請求,不等待IO結果,然後直接結束本輪消息處理,進入下一輪消息處理過程。當IO操作完成後,将收到一條“IO完成”的消息,處理該消息時就可以直接擷取IO操作結果。

在“發出IO請求”到收到“IO完成”的這段時間裡,同步IO模型下,主線程隻能挂起,但異步IO模型下,主線程并沒有休息,而是在消息循環中繼續處理其他消息。這樣,在異步IO模型下,一個線程就可以同時處理多個IO請求,并且沒有切換線程的操作。對于大多數IO密集型的應用程式,使用異步IO将大大提升系統的多任務處理能力。

asyncio

在 python3.5 中,建立一個協程僅僅隻需使用 async 關鍵字,而python3.4使用 @asyncio.coroutine 裝飾器。都引入了原生協程或者說異步生成器。下面的任一代碼,都可以作為協程工作,形式上也是等同的:

python異步IO程式設計(一)

3.1 中協程操作隻是簡單的生成器調用,常見的我們還需要在生成器或者說協程之間互相調用,用到yield from。yield from 用于一個generator調用另一個generator,主要是為了generator之間的調用。

yield from 表達式的使用方式如下:

協程的一個關鍵特性是它們可以被連結到一起。(記住,一個協程是可等待的,是以另一個協程可以使用 ​<code>​await​</code>​ 來等待它。)await 将控制器傳遞給時間循環。(挂起目前運作的協程與yield from類似),使用方式如下:

Python3.5 對這兩種調用協程的方法都提供了支援,但是推薦 async/await 作為首選。

Python執行的時候, g() 函數範圍内如果遇到表達式 await f(),就是 await 在告訴事件循環“挂起 g() 函數,直到 f() 傳回結果,在此期間,可以運作其他函數。”

當你使用 await f() 時,要求 f() 是一個可等待的對象。但這并沒有什麼用。現在,隻需要知道可等待對象要麼是(1)其他的協程,要麼就是(2)定義了 .await() 函數且傳回疊代器的對象。如果你正在編寫程式,絕大多數情況隻需要關注案例#1。

1. 使用 await 與 return 的組合建立協程函數。想要調用一個協程函數,必須使用 await 等待傳回結果。

2. 在 async def 代碼塊中使用 yield 的情況并不多見(隻有Python的近期版本才可用)。當你使用 async for 進行疊代的時候,會建立一個異步生成器。暫時先忘掉異步生成器,将目光放在使用 await 與 return 的組合建立協程函數的文法上。

3. 在任何使用 async def 定義的地方都不可以使用 yield from,這會引發異常 SyntaxError。

4. 一如在 def 定義的函數之外使用 yield 會引發異常 SyntaxError,在 async def 定義的協程之外使用 await 也會引發異常 SyntaxError。你隻能在協程内部使用 await。

python異步IO程式設計(一)

asyncio的程式設計模型就是一個消息循環。我們從asyncio子產品中直接擷取一個EventLoop的引用,然後把需要執行的協程扔到EventLoop中執行,就實作了異步IO。

用asyncio實作Hello world代碼如下:

@asyncio.coroutine把一個generator标記為coroutine類型,然後,我們就把這個coroutine扔到EventLoop中執行。

hello()會首先列印出Hello world!,然後,yield from文法可以讓我們友善地調用另一個generator。由于asyncio.sleep()也是一個coroutine,是以線程不會等待asyncio.sleep(),而是直接中斷并執行下一個消息循環。當asyncio.sleep()傳回時,線程就可以從yield from拿到傳回值(此處是None),然後接着執行下一行語句。

把asyncio.sleep(1)看成是一個耗時1秒的IO操作,在此期間,主線程并未等待,而是去執行EventLoop中其他可以執行的coroutine了,是以可以實作并發執行。

async/await版本:

我們用Task封裝兩個coroutine試試:

觀察執行過程:

Hello world! (&lt;_MainThread(MainThread, started 140735195337472)&gt;)

(暫停約1秒)

Hello again! (&lt;_MainThread(MainThread, started 140735195337472)&gt;)

由列印的目前線程名稱可以看出,兩個coroutine是由同一個線程并發執行的。

如果把asyncio.sleep()換成真正的IO操作,則多個coroutine就可以由一個線程并發執行。