Python 異步 IO 性能又上一層樓

最近看源碼的過程當中,發現了一個比較有意思的庫,aiomultiprocess,我認爲他確實是 Python 升級到 3.8 以後一個特性的總結庫,包括靜態檢查和性能提高。linux

這個是做者提供的一個 IO 性能對比圖:編程

由於其場景主要是圖片上傳,因此性能對比僅供參考,從邏輯上面推理,按照做者的思路確實能夠提高很多 IO 性能。api

做者實際上結合了 Python 的三個特性:微信

  • multiprocess網絡

  • asyncio多線程

  • map/reduce併發

這三個庫其實都是很普通的庫,使用過 Python3 以後的版本,基本上都會接觸到,那爲何做者能夠從這三個庫上面獲取到比較好的性能?主要是由於他在這三個庫的基礎上優化了協程的調度方式。app

優化後的協程調度方式更加相似於 Golang 的調度方式,惟一的差別是少了搶佔式調度的思路,僅僅使用了 RoundRobin 的方式,因此 IO 性能跟 Go 還有差距,可是確定比 Nodejs 的性能要好一些。異步

協程、線程、進程之間的區別

這個問題對於剛接觸的人來講很複雜。先說進程和線程,進程和線程都是操做系統的概念,線程必需要在進程裏面運行,另外進程是獨立的存儲空間,而線程的存儲空間是他所在進程的一部分,因此線程的開銷比進程小,線程的切換是邏輯上面的切換,可是進程的切換實際發生在物理 CPU 上面的;協程能夠認爲是比線程更加輕量的執行單元,他使用了線程的存儲空間,協程之間經過 function 級別的 namespace 區分存儲單元。綜合來看他們三者最大的差別是在所在的運行時以及切換的開銷。async

CPU 上面的 4 核 8 線程和咱們說的線程是什麼關係?4 核 8 線程的意思是每一個 CPU 核上面有兩個獨立的線程工做單元,因此 4 個核心一共有 8 個線程,這 8 個線程能夠看作是 8 條流水線,咱們上文討論的線程、進程、協程所有都是操做系統邏輯上面的概念,與這個關係不大。

在一個 4 核 8 線程的機器上面,理論上能夠同時運行 8 個進程。

若是進程數量大於 CPU 實際的線程數怎麼辦?操做系統進程有不少,開機就有 100+,那麼實際上咱們的 CPU 線程數是有限的,這 100+ 的系統進程須要按照不一樣的優先級由操做系統分時調度在不一樣的 CPU 線程上面工做。

如何理解並行和併發?並行和併發都是指事情同時發生,區別是...? emm 好像不是很好回答,惟一可以準確描述的是並行是獨立事件,併發是關聯事件。舉個例子:一條四車道的路能夠並排行駛 4 個車輛,可是若是這條路只有一車道的話,那麼也能夠行駛着四個車輛,區別是有的車輛須要排隊了,同時行駛的咱們認爲是並行,排隊走同一個車道的咱們認爲是併發。

當系統的進程數量 100 大於 CPU 的實際線程數量 8 時,那麼系統並行數是 8,併發數量是 100。這種方法一樣適用於描述線程和協程。

Python 爲何慢?

Python 解釋器是單進程的。一個 Python 解釋器是一個單進程的應用,再加上 GIL 鎖,致使在進程裏面不能並行執行線程,不一樣的線程是分時使用進程所在的 CPU 執行時間的。

由於 GIL 因此即便如今有充足的系統資源,那麼 Request 1,Request 2,Request 3 也沒有辦法同時發生。

動態類型。動態類型讓 Python 寫起來很是的簡單,全部的類型默認都是對象,因此編寫代碼很是的簡單直接,可是執行的時候,解釋器須要去推導類型,並動態檢查類型有沒有問題,因此致使運行時的任務比較重,相對於靜態編譯語言,速度跟不上。

協程的基本模型

協程實際上就是一個 function 級別的任務,分紅兩類,一類跟 IO 有關係,一類跟 IO 沒有關係。Golang 的優點是能很好的處理計算和 IO 相關的協程,目前在 Python 裏面勉強能夠解決跟 IO 相關的協程。

IO 其實是無法避免的,好比磁盤訪問,網絡 IO 等,他必定會有必定的等待時間,因此不管怎麼調度,都會有 IO 等待,爲了解決這個問題,操做系統引入了很多的模型:select、poll、epoll 等。

那麼基本上全部的編程語言都是利用了系統調用,也就是使用這些模型來解決問題,可是他們只可能比操做系統差,不能更好,由於有些編程語言誕生的比較早,大規模 IO 問題當時還沒遇到,說白了也就是那個時候沒有那麼多用戶。

我使用 Python 語言大體經歷了:進程 IO,線程 IO,協程 IO,協程池等,目前看到最優的就是 facebook 的這個 aiomultiprocess。

IO 協程也就是把 IO 封裝成 function 級別的單元來調度,而後利用操做系統的 IO 調度模型,提高應用程序的性能,相對於進程和線程來講對於系統資源的利用更加合理,並且存儲開銷更加小,調度開銷也小不少。

epoll 基本模型

epoll 是目前主流的 IO 調度模型,調度性能最優秀的實際上是微軟的 IOCP,可是由於閉源,並且系統應用開發通常集中在 linux 因此對他也瞭解的很少。

epoll 實際上把 IO 等待問題集中交給操做系統處理,並要求操做系統出現我關心的事件的時候提醒我(好比某個文件描述符能夠讀取,能夠寫入,出現了錯誤等),把 IO 問題交給操做系統以後,應用程序就能夠更加集中的去處理業務問題,而操做系統有更好的方式處理 IO,這就是站在了巨人的肩膀上。

Python 中的線程,進程,協程處理 IO 問題

Python 中使用進程處理問題至關於利用多核去等待 IO,相對於線程來講,使用了多核等待,線程是單核輪訓等待,使用多線程和多進程處理 IO 問題性能上面差別並非很大,可是系統開銷很大,特別的進程的開銷還要大於線程,很容易致使操做系統卡頓。

協程由於開銷小,因此能夠同時建立上萬的協程,因此其併發能力遠大於進程和線程,由於總須要等待 IO 時間,因此單進程內部不一樣的協程之間經過共享時間片的方式也是合理的。

AsyncIO 和協程之間的關係

AsyncIO 和協程之間並無什麼關係,可是他們一般同時出現,主要是 AsyncIO 須要依附於協程做爲容器,普通的協程只是在發生 IO 等待的時候交出了執行權限給其餘協程,告訴其餘人:我如今須要等待 IO 了,你先使用 CPU。可是在 AsyncIO 中,咱們使用 epoll 配合 IO api,實際的調度是 epoll 來管理 IO,普通的協程仍是利用 Python 應用程序管理 IO,因此本質上仍是有很大差異的。

aiomultiprocess 爲何更快?

aiomultiprocess 將異步 IO 和多進程結合起來了,很好的利用了多核和異步 IO 的優點,前面說到 Python 是單進程的,因此即便有 AsyncIO 那麼也並無充分利用操做系統的資源,相對來講還不是很快,可是結合了多進程以後,性能就能夠線性增加了。

Golang 爲何更快?

既然充分了利用了多核,那麼爲何仍是比 Golang 慢?主要仍是調度器的緣由,Golang 是搶佔式調度,目前 facebook 的這個版本主要是經過 RoundRobin 調度的,默認認爲每個任務和每個 worker 的執行時間是固定的,可是若是出現比較意外的狀況,好比系統資源佔用,就會致使進程出現假死,那麼依附在進程隊列上面的任務也不能獲得調度,爲此做者增長了進程的生命之週期管理 TTL,執行必定的任務以後,自毀並從新建立進程,這種方式能夠緩解調度問題,可是粒度仍是比較粗。

總結

aiomultiprocess 這個庫能夠說是很棒的一個 IO 模型庫了,合理的利用多核來完成 IO 任務,相對於單獨使用線程,進程,異步 IO 來講,會有一個不錯的提高,可普遍用於大規模的異步 IO 處理,好比發送郵件,上傳數據等。

本文分享自微信公衆號 - 茶歇小棧(smilehackerboy)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索