前面,我們從大的結構上認識了最新的Python 3.7裡面的asyncio標準庫。接下來,我們就開始一點一點的來學習asyncio的使用。
一、安裝 Python 3.7
我的系統是 Ubuntu 16.04,裡面有 Python 2.7 和 Python 3.6。2.7是系統自帶的,其它系統軟體對此有依賴,不能破壞。3.6 是透過ppa源apt install的,然後透過 virtualenvwrapper 管理虛擬環境。同樣的,安裝3.7也是用ppa源。
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.7 python3.7-dev
安裝python3.7-dev包的目的是編譯C/C++模組時需要Python.h等標頭檔案。安裝好後,建立一個Python 3.7 的虛擬環境,就叫 py3.7 吧:
mkvirtualenv -p python3.7 py3.7
建立好虛擬環境,透過命令:workon py3.7 就可以使用 3.7 了。
Windows 的安裝可以移步猿人學網站,閱讀《Python開發環境的安裝配置》這篇文章,裡面有詳細說明。
二、建立第一個協程
Python 3.7 推薦使用 async/await 語法來宣告協程,來編寫非同步應用程式。我們來建立第一個協程函式:首先列印一行“你好”,等待1秒鐘後再列印“猿人學”。
sayhi()函式透過 async 宣告為協程函式,較之前的修飾器宣告更簡潔明瞭。
在實踐過程中,什麼功能的函式要用async宣告為協程函式呢?就是那些能發揮非同步IO效能的函式,比如讀寫檔案、讀寫網路、讀寫資料庫,這些都是浪費時間的IO操作,把它們協程化、非同步化從而提高程式的整體效率(速度)。
sayhi()函式是透過 asyncio.run()來執行的,而不是直接呼叫這個函式(協程)。因為,直接呼叫並不會把它加入排程日程,而只是簡單的返回一個協程物件:
那麼,如何真正執行一個協程呢?asyncio 提供了三種機制:
(1)asyncio.run() 函式,這是非同步程式的主入口,相當於C語言中的main函式。
(2)用await等待協程,比如上例中的 await asyncio.sleep(1) 。再看下面的例子,我們定義了協程 say_delay() ,在main()協程中呼叫兩次,第一次延遲1秒後列印“你好”,第二次延遲2秒後列印“猿人學”。這樣我們透過 await 執行了兩個協程。
從起止時間可以看出,兩個協程是順序執行的,總共耗時1+2=3秒。
(3)透過 asyncio.create_task() 函式併發執行作為 asyncio 任務(Task) 的多個協程。下面,我們用create_task()來修改上面的main()協程,從而讓兩個say_delay()協程併發執行:
從執行結果的起止時間可以看出,兩個協程是併發執行的了,總耗時等於最大耗時2秒。
asyncio.create_task() 是一個很有用的函式,在爬蟲中它可以幫助我們實現大量併發去下載網頁。在Python 3.6中與它對應的是 ensure_future()。
三、可等待物件(awaitables)
可等待物件,就是可以在 await 表示式中使用的物件,前面我們已經接觸了兩種可等待物件的型別:協程和任務,還有一個是低層級的Future。
asyncio模組的許多API都需要傳入可等待物件,比如 run(), create_task() 等等。
(1)協程
協程是可等待物件,可以在其它協程中被等待。協程兩個緊密相關的概念是:
協程函式:透過 async def 定義的函式;
協程物件:呼叫協程函式返回的物件。
執行上面這段程式,結果為:
co is
now is 1548512708.2026224
now is 1548512708.202648
可以看到,直接執行協程函式 whattime()得到的co是一個協程物件,因為協程物件是可等待的,所以透過 await 得到真正的當前時間。now2是直接await 協程函式,也得到了當前時間的返回值。
(2)任務
前面我們講到,任務是用來排程協程的,以便併發執行協程。當一個協程透過 asyncio.create_task() 被打包為一個 任務,該協程將自動加入程式排程日程準備立即執行。
create_task()的基本使用前面例子已經講過。它返回的task透過await來等待其執行完。如果,我們不等待,會發生什麼?“準備立即執行”又該如何理解呢?先看看下面這個例子:
執行這段程式碼的情況是這樣的:
首先,1秒鐘後列印一行,這是第13,14行程式碼執行的結果:
calling:0, now is 09:15:15
接著,停頓1秒後,連續列印4行:
calling:1, now is 09:15:16
calling:2, now is 09:15:16
calling:3, now is 09:15:16
calling:4, now is 09:15:16
從這個結果看,asyncio.create_task()產生的4個任務,我們並沒有await,它們也執行了。關鍵在於第18行的 await,如果把這一行去掉或是sleep的時間小於1秒(比whattime()裡面的sleep時間少即可),就會只看到第一行的輸出結果而看不到後面四行的輸出。這是因為,main()不sleep或sleep少於1秒鐘,main()就在whattime()還未來得及列印結果(因為,它要sleep1秒)就退出了,從而整個程式也退出了,就沒有whattime()的輸出結果。
再來理解一下“準備立即執行”這個說法。它的意思就是,create_task()只是打包了協程並加入排程佇列還未執行,並準備立即執行,什麼時候執行呢?在“主協程”(呼叫create_task()的協程)掛起的時候,這裡的“掛起”有兩個方式:
一是,透過 await task 來執行這個任務;
另一個是,主協程透過 await sleep 掛起,事件迴圈就去執行task了。
我們知道,asyncio是透過事件迴圈實現非同步的。在主協程 main()裡面,沒有遇到 await 時,事件就是執行main()函式,遇到 await 時,事件迴圈就去執行別的協程,即create_task()生成的whattime()的4個任務,這些任務一開始就是 await sleep 1秒。這時候,主協程和4個任務協程都掛起了,CPU空閒,事件迴圈等待協程的訊息。
如果main()協程只sleep了0.1秒,它就先醒了,給事件迴圈發訊息,事件迴圈就來繼續執行main()協程,而main()後面已經沒有程式碼,就退出該協程,退出它也就意味著整個程式退出,4個任務就沒機會列印結果;
如果main()協程sleep時間多餘1秒,那麼4個任務先喚醒,就會得到全部的列印結果;
如果main()的18行sleep等於1秒時,和4個任務的sleep時間相同,也會得到全部列印結果。這是為什麼呢?
我猜想是這樣的:4個任務生成在前,第18行的sleep在後,事件迴圈的訊息響應可能有個先進先出的順序。後面深入asyncio的程式碼專門研究一下這個猜想正確與否。
(3)Future
它是一個低層級的可等待物件,表示一個非同步操作的最終結果。目前,我們寫應用程式還用不到它,暫不學習。
asyncio非同步IO協程總結
協程就是我們非同步操作的片段。通常,寫程式都會把全部功能分成很多不同功能的函式,目的是為了結構清晰;進一步,把那些涉及耗費時間的IO操作(讀寫檔案、資料庫、網路)的函式透過 async def 非同步化,就是非同步程式設計。
那些非同步函式(協程函式)都是透過訊息機制被事件迴圈管理排程著,整個程式的執行是單執行緒的,但是某個協程A進行IO時,事件迴圈就去執行其它協程非IO的程式碼。當事件迴圈收到協程A結束IO的訊息時,就又回來執行協程A,這樣事件迴圈不斷在協程之間轉換,充分利用了IO的閒置時間,從而併發的進行多個IO操作,這就是非同步IO。
寫非同步IO程式時記住一個準則:需要IO的地方非同步。其它地方即使用了協程函式也是沒用的。
網路爬蟲就是非同步IO的用武之地,接下來的文章,我們就來實現一個非同步IO爬蟲,敬請期待。
非同步IO系列閱讀:
深入瞭解Python的非同步IO:概念和歷史
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***