天天看點

Python中的異步程式設計:Asyncio

如果你已經決定要了解 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 隻是調用協程的下一步。

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 的中心執行器。

現在我們看一下所有這些如何融為一體。正如我之前提到的,異步代碼在一個線程中運作。

Python中的異步程式設計:Asyncio

從上圖可知:

1.消息循環是線上程中執行

2.從隊列中取得任務

3.每個任務在協程中執行下一步動作

4.如果在一個協程中調用另一個協程(await <coroutine_name>),會觸發上下文切換,挂起目前協程,并儲存現場環境(變量,狀态),然後載入被調用協程

5.如果協程的執行到阻塞部分(阻塞i/o,sleep),目前協程會挂起,并将控制權傳回到線程的消息循環中,然後消息循環繼續從隊列中執行下一個任務...以此類推

6.隊列中的所有任務執行完畢後,消息循環傳回第一個任務

異步和同步的代碼對比

現在我們實際驗證異步模式的切實有效,我會比較兩段 python 腳本,這兩個腳本除了sleep 方法外,其餘部分完全相同。在第一個腳本裡,我會用标準的 time.sleep 方法,在第二個腳本裡使用 asyncio.sleep 的異步方法。

這裡使用 sleep 是因為它是一個用來展示異步方法如何操作 i/o 的最簡單辦法。

使用同步 sleep 方法的代碼:

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))  

腳本輸出:

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 的代碼:

    print('sleep {}\n'.format(datetime.now())) 

    await asyncio.sleep(1) 

sleep 2017-04-06 13:44:40.648665 

sleep 2017-04-06 13:44:40.648859 

sleep 2017-04-06 13:44:41.649564 

sleep 2017-04-06 13:44:41.649943 

sleep 2017-04-06 13:44:42.651755 

total time: 3.008226156234741  

從輸出可以看到,異步模式的代碼執行速度快了大概兩秒。當使用異步模式的時候(每次調用 await asyncio.sleep(1) ),程序控制權會傳回到主程式的消息循環裡,并開始運作隊列的其他任務(任務a或者任務b)。

當使用标準的 sleep方法時,目前線程會挂起等待。什麼也不會做。實際上,标準的 sleep 過程中,目前線程也會傳回一個 python 的解釋器,可以操作現有的其他線程,但這是另一個話題了。

推薦使用異步模式程式設計的幾個理由

很多公司的産品都廣泛的使用了異步模式,如 facebook 旗下著名的 react native 和 rocksdb 。像 twitter

每天可以承載 50 億的使用者通路,靠的也是異步模式程式設計。是以說,通過代碼重構,或者改變模式方法,就能讓系統工作的更快,為什麼不去試一下呢?

作者:佚名

來源:51cto