咱們講以Python 3.7 上的asyncio爲例講解如何使用Python的異步IO。數據庫
Python 3.7 推薦使用 async/await 語法來聲明協程,來編寫異步應用程序。咱們來建立第一個協程函數:首先打印一行「你好」,等待1秒鐘後再打印「猿人學」。編程
sayhi()函數經過 async 聲明爲協程函數,較以前的修飾器聲明更簡潔明瞭。網絡
在實踐過程當中,什麼功能的函數要用async聲明爲協程函數呢?就是那些能發揮異步IO性能的函數,好比讀寫文件、讀寫網絡、讀寫數據庫,這些都是浪費時間的IO操做,把它們協程化、異步化從而提升程序的總體效率(速度)。併發
sayhi()函數是經過 asyncio.run()
來運行的,而不是直接調用這個函數(協程)。由於,直接調用並不會把它加入調度日程,而只是簡單的返回一個協程對象:異步
那麼,如何真正運行一個協程呢?asyncio 提供了三種機制:async
(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()
。
可等待對象,就是能夠在 await 表達式中使用的對象,前面咱們已經接觸了兩種可等待對象的類型:協程和任務,還有一個是低層級的Future。
asyncio模塊的許多API都須要傳入可等待對象,好比 run(), create_task() 等等。
協程是可等待對象,能夠在其它協程中被等待。協程兩個緊密相關的概念是:
運行上面這段程序,結果爲:
co is now is 1548512708.2026224 now is 1548512708.202648
能夠看到,直接運行協程函數 whattime()獲得的co是一個協程對象,由於協程對象是可等待的,因此經過 await 獲得真正的當前時間。now2是直接await 協程函數,也獲得了當前時間的返回值。
前面咱們講到,任務是用來調度協程的,以便併發執行協程。當一個協程經過 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()還將來得及打印結果(由於,它要sleep 1秒)就退出了,從而整個程序也退出了,就沒有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的代碼專門研究一下這個猜測正確與否。
它是一個低層級的可等待對象,表示一個異步操做的最終結果。目前,咱們寫應用程序還用不到它,暫不學習。
協程就是咱們異步操做的片斷。一般,寫程序都會把所有功能分紅不少不一樣功能的函數,目的是爲告終構清晰;進一步,把那些涉及耗費時間的IO操做(讀寫文件、數據庫、網絡)的函數經過 async def 異步化,就是異步編程。
那些異步函數(協程函數)都是經過消息機制被事件循環管理調度着,整個程序的執行是單線程的,可是某個協程A進行IO時,事件循環就去執行其它協程非IO的代碼。當事件循環收到協程A結束IO的消息時,就又回來執行協程A,這樣事件循環不斷在協程之間轉換,充分利用了IO的閒置時間,從而併發的進行多個IO操做,這就是異步IO。
寫異步IO程序時記住一個準則:須要IO的地方異步。其它地方即便用了協程函數也是沒用的。