如果你已經決定要理解 Python 的非同步部分,歡迎來到我們的“Asyncio How-to ”。
注:哪怕連異動正規化的存在都不知道的情況下,你也可以成功地使用 Python。但是,如果你對底層執行模式感興趣的話,asyncio 絕對值得檢視。
非同步是怎麼一回事?
在傳統的順序程式設計中, 所有傳送給直譯器的指令會一條條被執行。此類程式碼的輸出容易顯現和預測。 但是…
譬如說你有一個指令碼向3個不同伺服器請求資料。 有時,誰知什麼原因,傳送給其中一個伺服器的請求可能意外地執行了很長時間。想象一下從第二個伺服器獲取資料用了10秒鐘。在你等待的時候,整個指令碼實際上什麼也沒幹。如果你可以寫一個指令碼可以不去等待第二個請求而是僅僅跳過它,然後開始執行第三個請求,然後回到第二個請求,執行之前離開的位置會怎麼樣呢。就是這樣。你通過切換任務最小化了空轉時間。儘管如此,當你需要一個幾乎沒有I/O的簡單指令碼時,你不想用非同步程式碼。
還有一件重要的事情要提,所有程式碼在一個執行緒中執行。所以如果你想讓程式的一部分在後臺執行同時幹一些其他事情,那是不可能的。
準備開始
這是 asyncio 主概念最基本的定義:
- 協程 — 消費資料的生成器,但是不生成資料。Python 2.5 介紹了一種新的語法讓傳送資料到生成器成為可能。我推薦查閱David Beazley “A Curious Course on Coroutines and Concurrency” 關於協程的詳細介紹。
- 任務 — 協程排程器。如果你觀察下面的程式碼,你會發現它只是讓 event_loop 儘快呼叫它的_step ,同時 _step 只是呼叫協程的下一步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Task(futures.Future): def __init__(self, coro, loop=None): super().__init__(loop=loop) ... self._loop.call_soon(self._step) def _step(self): ... try: ... result = next(self._coro) except StopIteration as exc: self.set_result(exc.value) except BaseException as exc: self.set_exception(exc) raise else: ... self._loop.call_soon(self._step) |
- 事件迴圈 — 把它想成 asyncio 的中心執行器。
現在我們看一下所有這些如何融為一體。正如我之前提到的,非同步程式碼在一個執行緒中執行。
從上圖可知:
1.訊息迴圈是線上程中執行
2.從佇列中取得任務
3.每個任務在協程中執行下一步動作
4.如果在一個協程中呼叫另一個協程(await <coroutine_name>),會觸發上下文切換,掛起當前協程,並儲存現場環境(變數,狀態),然後載入被呼叫協程
5.如果協程的執行到阻塞部分(阻塞I/O,Sleep),當前協程會掛起,並將控制權返回到執行緒的訊息迴圈中,然後訊息迴圈繼續從佇列中執行下一個任務...以此類推
6.佇列中的所有任務執行完畢後,訊息迴圈返回第一個任務
非同步和同步的程式碼對比
現在我們實際驗證非同步模式的切實有效,我會比較兩段 python 指令碼,這兩個指令碼除了sleep 方法外,其餘部分完全相同。在第一個指令碼里,我會用標準的 time.sleep 方法,在第二個指令碼里使用 asyncio.sleep 的非同步方法。
這裡使用 Sleep 是因為它是一個用來展示非同步方法如何操作 I/O 的最簡單辦法。
使用同步 sleep 方法的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import asyncio import time from datetime import datetime async def custom_sleep(): print('SLEEP', datetime.now()) time.sleep(1) async def factorial(name, number): f = 1 for i in range(2, number+1): print('Task {}: Compute factorial({})'.format(name, i)) await custom_sleep() f *= i print('Task {}: factorial({}) is {}\n'.format(name, number, f)) start = time.time() loop = asyncio.get_event_loop() tasks = [ asyncio.ensure_future(factorial("A", 3)), asyncio.ensure_future(factorial("B", 4)), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() end = time.time() print("Total time: {}".format(end - start)) |
指令碼輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Task A: Compute factorial(2) SLEEP 2017-04-06 13:39:56.207479 Task A: Compute factorial(3) SLEEP 2017-04-06 13:39:57.210128 Task A: factorial(3) is 6 Task B: Compute factorial(2) SLEEP 2017-04-06 13:39:58.210778 Task B: Compute factorial(3) SLEEP 2017-04-06 13:39:59.212510 Task B: Compute factorial(4) SLEEP 2017-04-06 13:40:00.217308 Task B: factorial(4) is 24 Total time: 5.016386032104492 |
使用非同步 Sleep 的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import asyncio import time from datetime import datetime async def custom_sleep(): print('SLEEP {}\n'.format(datetime.now())) await asyncio.sleep(1) async def factorial(name, number): f = 1 for i in range(2, number+1): print('Task {}: Compute factorial({})'.format(name, i)) await custom_sleep() f *= i print('Task {}: factorial({}) is {}\n'.format(name, number, f)) start = time.time() loop = asyncio.get_event_loop() tasks = [ asyncio.ensure_future(factorial("A", 3)), asyncio.ensure_future(factorial("B", 4)), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() end = time.time() print("Total time: {}".format(end - start)) |
指令碼輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Task A: Compute factorial(2) SLEEP 2017-04-06 13:44:40.648665 Task B: Compute factorial(2) SLEEP 2017-04-06 13:44:40.648859 Task A: Compute factorial(3) SLEEP 2017-04-06 13:44:41.649564 Task B: Compute factorial(3) SLEEP 2017-04-06 13:44:41.649943 Task A: factorial(3) is 6 Task B: Compute factorial(4) SLEEP 2017-04-06 13:44:42.651755 Task B: factorial(4) is 24 Total time: 3.008226156234741 |
從輸出可以看到,非同步模式的程式碼執行速度快了大概兩秒。當使用非同步模式的時候(每次呼叫 await asyncio.sleep(1) ),程式控制權會返回到主程式的訊息迴圈裡,並開始執行佇列的其他任務(任務A或者任務B)。
當使用標準的 sleep方法時,當前執行緒會掛起等待。什麼也不會做。實際上,標準的 sleep 過程中,當前執行緒也會返回一個 python 的直譯器,可以操作現有的其他執行緒,但這是另一個話題了。
推薦使用非同步模式程式設計的幾個理由
很多公司的產品都廣泛的使用了非同步模式,如 Facebook 旗下著名的 React Native 和 RocksDB 。像 Twitter 每天可以承載 50 億的使用者訪問,靠的也是非同步模式程式設計。所以說,通過程式碼重構,或者改變模式方法,就能讓系統工作的更快,為什麼不去試一下呢?