Python中的異步協程怎麼學?看完這篇文章你基本就會了

01.什麼是協程

協程,英文叫做 Coroutine,又稱微線程,纖程,協程是一種用戶態的輕量級線程。

協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。

我們可以使用協程來實現異步操作,比如在網絡爬蟲場景下,我們發出一個請求之後,需要等待一定的時間才能得到響應,但其實在這個等待過程中,程序可以幹許多其他的事情,等到響應得到之後才切換回來繼續處理,這樣可以充分利用 CPU 和其他資源,這就是異步協程的優勢。

02.定義協程

首先我們來定義一個協程,體驗一下它和普通進程在實現上的不同之處,代碼如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

運行結果:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

1).先import Python裡面大名鼎鼎的asyncio這個異步包,然後才可以使用 async 和 await。我們接著 async 定義了一個 execute() 方法,方法接收一個數字參數,方法執行之後會打印這個數字。

2).隨後我們直接調用了這個方法,然而這個方法並沒有執行,而是返回了一個 coroutine 協程對象。

3).隨後我們使用 get_event_loop() 方法創建了一個事件循環 loop,並調用了 loop 對象的 run_until_complete() 方法將協程註冊到事件循環 loop 中,然後啟動。最後我們才看到了 execute() 方法打印了輸出結果。

4).可見,async 定義的方法就會變成一個無法直接執行的 coroutine 對象,必須將其註冊到事件循環中才可以執行。

5).上文我們還提到了 task,它是對 coroutine 對象的進一步封裝,它裡面相比 coroutine 對象多了運行狀態,比如 running、finished 等,我們可以用這些狀態來獲取協程對象的執行情況。

在上面的例子中,當我們將 coroutine 對象傳遞給 run_until_complete() 方法的時候,實際上它進行了一個操作就是將 coroutine 封裝成了 task 對象,我們也可以顯式地進行聲明,如下所示:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

運行結果:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

  • 這裡我們定義了 loop 對象之後,接著調用了它的 create_task() 方法將 coroutine 對象轉化為了 task 對象
  • 隨後我們打印輸出一下,發現它是 pending 狀態。接著我們將 task 對象添加到事件循環中得到執行,
  • 隨後我們再打印輸出一下 task 對象,發現它的狀態就變成了 finished
  • 同時還可以看到其 result 變成了 1,也就是我們定義的 execute() 方法的返回結果。

另外定義 task 對象還有一種方式,就是直接通過 asyncio 的 ensure_future() 方法,返回結果也是 task 對象,這樣的話我們就可以不借助於 loop 來定義,即使我們還沒有聲明 loop 也可以提前定義好 task 對象,寫法如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

運行結果:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

發現其效果都是一樣的。

03.綁定回調

另外我們也可以為某個 task 綁定一個回調方法,來看下面的例子:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

  • 在這裡我們定義了一個 request() 方法,請求了百度,返回狀態碼,但是這個方法裡面我們沒有任何 print() 語句。
  • 隨後我們定義了一個 callback() 方法,這個方法接收一個參數,是 task 對象,然後調用 print() 方法打印了 task 對象的結果。
  • 這樣我們就定義好了一個 coroutine 對象和一個回調方法,我們現在希望的效果是,當 coroutine 對象執行完畢之後,就去執行聲明的 callback() 方法。

那麼它們二者怎樣關聯起來呢?很簡單,只需要調用 add_done_callback() 方法即可,我們將 callback() 方法傳遞給了封裝好的 task 對象,這樣當 task 執行完畢之後就可以調用 callback() 方法了。

同時 task 對象還會作為參數傳遞給 callback() 方法,調用 task 對象的 result() 方法就可以獲取返回結果了。

運行結果:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

實際上不用回調方法,直接在 task 運行完畢之後也可以直接調用 result() 方法獲取結果,如下所示:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

運行結果是一樣的:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

04.多任務協程

上面的例子我們只執行了一次請求,如果我們想執行多次請求應該怎麼辦呢?我們可以定義一個 task 列表,然後使用 asyncio 的 wait() 方法即可執行,看下面的例子:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

這裡我們使用一個 for 循環創建了五個 task,組成了一個列表,然後把這個列表首先傳遞給了 asyncio 的 wait() 方法,然後再將其註冊到時間循環中,就可以發起五個任務了。最後我們再將任務的運行結果輸出出來,運行結果如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

可以看到五個任務被順次執行了,並得到了運行結果。

05.一個實戰爬蟲例子,協程實現

前面說了這麼一通,又是 async,又是 coroutine,又是 task,又是 callback,但似乎並沒有看出協程的優勢啊?反而寫法上更加奇怪和麻煩了!

為了表現出協程的優勢,最好的方法就是模擬一個需要等待一定時間才可以獲取返回結果的網頁,最好的方式是自己在本地模擬一個慢速服務器,這裡我們選用 Flask,然後編寫服務器代碼如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

這裡我們定義了一個 Flask 服務,主入口是 index() 方法,方法裡面先調用了 sleep() 方法休眠 3 秒,然後接著再返回結果,也就是說,每次請求這個接口至少要耗時 3 秒,這樣我們就模擬了一個慢速的服務接口。

注意這裡服務啟動的時候,run() 方法加了一個參數 threaded,這表明 Flask 啟動了多線程模式,不然默認是隻有一個線程的。

如果不開啟多線程模式,同一時刻遇到多個請求的時候,只能順次處理,這樣即使我們使用協程異步請求了這個服務,也只能一個一個排隊等待,瓶頸就會出現在服務端。所以,多線程模式是有必要打開的。

啟動之後,Flask 應該默認會在 127.0.0.1:5000 上運行,運行之後控制檯輸出結果如下:

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

接下來我們再重新使用上面的方法請求一遍:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

在這裡我們還是創建了五個 task,然後將 task 列表傳給 wait() 方法並註冊到時間循環中執行。

運行結果如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

可以發現和正常的請求並沒有什麼兩樣,依然還是順次執行的,耗時 15 秒,平均一個請求耗時 3 秒,說好的異步處理呢?

其實,要實現異步處理,我們得先要有掛起的操作,當一個任務需要等待 IO 結果的時候,可以掛起當前任務,轉而去執行其他任務,這樣我們才能充分利用好資源,上面方法都是一本正經的串行走下來,連個掛起都沒有,怎麼可能實現異步?想太多了。

要實現異步,接下來我們再瞭解一下 await 的用法,使用 await 可以將耗時等待的操作掛起,讓出控制權。當協程執行的時候遇到 await,時間循環就會將本協程掛起,轉而去執行別的協程,直到其他的協程掛起或執行完畢。

所以,我們可能會將代碼中的 request() 方法改成如下的樣子:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

僅僅是在 requests 前面加了一個 await,然而執行以下代碼,會得到如下報錯:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

這次它遇到 await 方法確實掛起了,也等待了,但是最後卻報了這麼個錯,這個錯誤的意思是 requests 返回的 Response 對象不能和 await 一起使用,為什麼呢?因為根據官方文檔說明,await 後面的對象必須是如下格式之一:

  • A native coroutine object returned from a native coroutine function,一個原生 coroutine 對象。
  • A generator-based coroutine object returned from a function decorated with types.coroutine(),一個由 types.coroutine() 修飾的生成器,這個生成器可以返回 coroutine 對象。
  • An object with an await__ method returning an iterator,一個包含 __await 方法的對象返回的一個迭代器。

reqeusts 返回的 Response 不符合上面任一條件,因此就會報上面的錯誤了。那麼有的小夥伴就發現了,既然 await 後面可以跟一個 coroutine 對象,那麼我用 async 把請求的方法改成 coroutine 對象不就可以了嗎?所以就改寫成如下的樣子:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

這裡我們將請求頁面的方法獨立出來,並用 async 修飾,這樣就得到了一個 coroutine 對象,我們運行一下看看:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

還是不行,它還不是異步執行,也就是說我們僅僅將涉及 IO 操作的代碼封裝到 async 修飾的方法裡面是不可行的!我們必須要使用支持異步操作的請求方式才可以實現真正的異步,所以這裡就需要 aiohttp 派上用場了。

06.使用 aiohttp

aiohttp 是一個支持異步請求的庫,利用它和 asyncio 配合我們可以非常方便地實現異步請求操作。我們將 aiohttp 用上來,將代碼改成如下樣子:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

在這裡我們將請求庫由 requests 改成了 aiohttp,通過 aiohttp 的 ClientSession 類的 get() 方法進行請求,結果如下:

Python中的異步協程怎麼學?看完這篇文章你基本就會了

成功了!我們發現這次請求的耗時由 15 秒變成了 3 秒,耗時直接變成了原來的 1/5。

這篇文章是崔慶才的大作,雖然很長,乾貨很多,需要慢慢消化,裡面的關鍵字非常多,大家動手敲敲代碼練習一下,方能體會Python協程的巧妙之處,也歡迎大家留言討論!

一字不拉,全部看完的同學,請舉手!

文末小知識點摘要:Python 核心開發者:Python 之父的退休沒有帶來影響

距 Python 創始人 Guido van Rossum 宣佈脫離 Python 決策層辭去 BDFL 身份已有一個多月時間,Python 內部究竟有何變化?在上週末舉辦的 PyBay 2018 大會上,Python 核心開發團隊成員、顧問兼演講者 Raymond Hettinger 在接受外媒 The Register 採訪時表示,Python 之父的退休並沒有真正改變什麼。

Guido 的退休沒有對 Python 的發展進程帶來改變。Guido 給我們留下了一個“自治”的挑戰,而在自治這一點上,目前沒有具備爭議和難度的東西需要解決。

另一位技術指導講師 Simeon Franklin 也表示,如今的 Python 是由 30-40 人一起在管理的 —— “Nothing is really changing that much”。

根據 Hettinger 所述,目前 Python 代碼 check in 和 bug 修復的速度與 Guido 離開之前基本相同。Guido 本身在歷史上的角色也是在一些語言發展方向上做出決定的人,而不是管理流程的人。

Hettiner 補充道,核心開發團隊即將迎來爆發,因為沒有了 Guido van Rossum 作為仲裁者,該團隊需要擔負更多責任。在過去,他們可以有相對瘋狂的想法,因為他們知道如果有什麼東西超過了底線,屋子裡會有一個成年人說,“不,我們不能那樣做”。但現在,他們需要自己做決定。

Python中的異步協程怎麼學?看完這篇文章你基本就會了

希望今天的分享對你會有所幫助,想要學習Python,點個關注收藏一下。


分享到:


相關文章: