| 前面,我們從大的結構上認識了最新的Python 3.7里面的asyncio標準庫。接下來,我們就開始一點一點的來學習asyncio的使用。 一、安裝 Python 3.7 我的系統(tǒng)是 Ubuntu 16.04,里面有 Python 2.7 和 Python 3.6。2.7是系統(tǒng)自帶的,其它系統(tǒng)軟件對此有依賴,不能破壞。3.6 是通過ppa源apt install的,然后通過 virtualenvwrapper 管理虛擬環(huán)境。同樣的,安裝3.7也是用ppa源。 sudo add-apt-repository ppa:deadsnakes/ppasudo apt update 安裝python3.7-dev包的目的是編譯C/C++模塊時需要Python.h等頭文件。安裝好后,建立一個Python 3.7 的虛擬環(huán)境,就叫 py3.7 吧: mkvirtualenv -p python3.7 py3.7 建立好虛擬環(huán)境,通過命令:workon py3.7 就可以使用 3.7 了。 Windows 的安裝可以移步猿人學網(wǎng)站,閱讀《Python開發(fā)環(huán)境的安裝配置》這篇文章,里面有詳細說明。 二、創(chuàng)建第一個協(xié)程 Python 3.7 推薦使用 async/await 語法來聲明協(xié)程,來編寫異步應用程序。我們來創(chuàng)建第一個協(xié)程函數(shù):首先打印一行“你好”,等待1秒鐘后再打印“猿人學”。 sayhi()函數(shù)通過 async 聲明為協(xié)程函數(shù),較之前的修飾器聲明更簡潔明了。 在實踐過程中,什么功能的函數(shù)要用async聲明為協(xié)程函數(shù)呢?就是那些能發(fā)揮異步IO性能的函數(shù),比如讀寫文件、讀寫網(wǎng)絡、讀寫數(shù)據(jù)庫,這些都是浪費時間的IO操作,把它們協(xié)程化、異步化從而提高程序的整體效率(速度)。 sayhi()函數(shù)是通過 asyncio.run()來運行的,而不是直接調用這個函數(shù)(協(xié)程)。因為,直接調用并不會把它加入調度日程,而只是簡單的返回一個協(xié)程對象: 那么,如何真正運行一個協(xié)程呢?asyncio 提供了三種機制: (1)asyncio.run() 函數(shù),這是異步程序的主入口,相當于C語言中的main函數(shù)。 (2)用await等待協(xié)程,比如上例中的 await asyncio.sleep(1) 。再看下面的例子,我們定義了協(xié)程 say_delay() ,在main()協(xié)程中調用兩次,第一次延遲1秒后打印“你好”,第二次延遲2秒后打印“猿人學”。這樣我們通過 await 運行了兩個協(xié)程。 從起止時間可以看出,兩個協(xié)程是順序執(zhí)行的,總共耗時1+2=3秒。 (3)通過 asyncio.create_task() 函數(shù)并發(fā)運行作為 asyncio 任務(Task) 的多個協(xié)程。下面,我們用create_task()來修改上面的main()協(xié)程,從而讓兩個say_delay()協(xié)程并發(fā)運行: 從運行結果的起止時間可以看出,兩個協(xié)程是并發(fā)執(zhí)行的了,總耗時等于最大耗時2秒。 asyncio.create_task() 是一個很有用的函數(shù),在爬蟲中它可以幫助我們實現(xiàn)大量并發(fā)去下載網(wǎng)頁。在Python 3.6中與它對應的是 ensure_future()。 三、可等待對象(awaitables) 可等待對象,就是可以在 await 表達式中使用的對象,前面我們已經(jīng)接觸了兩種可等待對象的類型:協(xié)程和任務,還有一個是低層級的Future。 asyncio模塊的許多API都需要傳入可等待對象,比如 run(), create_task() 等等。 (1)協(xié)程 協(xié)程是可等待對象,可以在其它協(xié)程中被等待。協(xié)程兩個緊密相關的概念是: 
 運行上面這段程序,結果為: co is <class 'coroutine'> now is 1548512708.2026224 now is 1548512708.202648 可以看到,直接運行協(xié)程函數(shù) whattime()得到的co是一個協(xié)程對象,因為協(xié)程對象是可等待的,所以通過 await 得到真正的當前時間。now2是直接await 協(xié)程函數(shù),也得到了當前時間的返回值。 (2)任務 前面我們講到,任務是用來調度協(xié)程的,以便并發(fā)執(zhí)行協(xié)程。當一個協(xié)程通過 asyncio.create_task() 被打包為一個 任務,該協(xié)程將自動加入程序調度日程準備立即運行。 create_task()的基本使用前面例子已經(jīng)講過。它返回的task通過await來等待其運行完。如果,我們不等待,會發(fā)生什么?“準備立即運行”又該如何理解呢?先看看下面這個例子: 運行這段代碼的情況是這樣的: 首先,1秒鐘后打印一行,這是第13,14行代碼運行的結果: calling:0, now is 09:15:15  接著,停頓1秒后,連續(xù)打印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()產(chǎn)生的4個任務,我們并沒有await,它們也執(zhí)行了。關鍵在于第18行的 await,如果把這一行去掉或是sleep的時間小于1秒(比whattime()里面的sleep時間少即可),就會只看到第一行的輸出結果而看不到后面四行的輸出。這是因為,main()不sleep或sleep少于1秒鐘,main()就在whattime()還未來得及打印結果(因為,它要sleep1秒)就退出了,從而整個程序也退出了,就沒有whattime()的輸出結果。 再來理解一下“準備立即執(zhí)行”這個說法。它的意思就是,create_task()只是打包了協(xié)程并加入調度隊列還未執(zhí)行,并準備立即執(zhí)行,什么時候執(zhí)行呢?在“主協(xié)程”(調用create_task()的協(xié)程)掛起的時候,這里的“掛起”有兩個方式: 一是,通過 await task 來執(zhí)行這個任務; 另一個是,主協(xié)程通過 await sleep 掛起,事件循環(huán)就去執(zhí)行task了。 我們知道,asyncio是通過事件循環(huán)實現(xiàn)異步的。在主協(xié)程 main()里面,沒有遇到 await 時,事件就是執(zhí)行main()函數(shù),遇到 await 時,事件循環(huán)就去執(zhí)行別的協(xié)程,即create_task()生成的whattime()的4個任務,這些任務一開始就是 await sleep 1秒。這時候,主協(xié)程和4個任務協(xié)程都掛起了,CPU空閑,事件循環(huán)等待協(xié)程的消息。 如果main()協(xié)程只sleep了0.1秒,它就先醒了,給事件循環(huán)發(fā)消息,事件循環(huán)就來繼續(xù)執(zhí)行main()協(xié)程,而main()后面已經(jīng)沒有代碼,就退出該協(xié)程,退出它也就意味著整個程序退出,4個任務就沒機會打印結果; 如果main()協(xié)程sleep時間多余1秒,那么4個任務先喚醒,就會得到全部的打印結果; 如果main()的18行sleep等于1秒時,和4個任務的sleep時間相同,也會得到全部打印結果。這是為什么呢? 我猜想是這樣的:4個任務生成在前,第18行的sleep在后,事件循環(huán)的消息響應可能有個先進先出的順序。后面深入asyncio的代碼專門研究一下這個猜想正確與否。 (3)Future 它是一個低層級的可等待對象,表示一個異步操作的最終結果。目前,我們寫應用程序還用不到它,暫不學習。 總結 協(xié)程就是我們異步操作的片段。通常,寫程序都會把全部功能分成很多不同功能的函數(shù),目的是為了結構清晰;進一步,把那些涉及耗費時間的IO操作(讀寫文件、數(shù)據(jù)庫、網(wǎng)絡)的函數(shù)通過 async def 異步化,就是異步編程。 那些異步函數(shù)(協(xié)程函數(shù))都是通過消息機制被事件循環(huán)管理調度著,整個程序的執(zhí)行是單線程的,但是某個協(xié)程A進行IO時,事件循環(huán)就去執(zhí)行其它協(xié)程非IO的代碼。當事件循環(huán)收到協(xié)程A結束IO的消息時,就又回來執(zhí)行協(xié)程A,這樣事件循環(huán)不斷在協(xié)程之間轉換,充分利用了IO的閑置時間,從而并發(fā)的進行多個IO操作,這就是異步IO。 寫異步IO程序時記住一個準則:需要IO的地方異步。其它地方即使用了協(xié)程函數(shù)也是沒用的。 網(wǎng)絡爬蟲就是異步IO的用武之地,接下來的文章,我們就來實現(xiàn)一個異步IO爬蟲,敬請期待。 | 
|  |