終於搞明白了,異步Python比同步Python究竟快在哪裏?

你們好,你是否聽人們說過,異步 Python 代碼比「普通(或同步)Python 代碼更快? 果然是那樣嗎?html

同步和異步是什麼意思?

Web 應用程序一般要處理許多請求,這些請求在短期內來自不一樣的客戶端。爲避免處理延遲,必須考慮並行處理多個請求,這一般稱爲「併發」。python

在本文中,我將繼續使用 Web 應用程序做爲例子,但還有其它類型的應用程序也從併發中獲益。所以,這個討論並不只僅是針對 Web 應用程序的。web

術語「同步」和「異步」指的是編寫併發應用程序的兩種方式。所謂的「同步」服務器使用底層操做系統支持的線程和進程來實現這種併發性。下面是同步部署的一個示意圖:數據庫

在這種狀況下,咱們有 5 臺客戶端,都向應用程序發送請求。這個應用程序的訪問入口是一個 Web 服務器,經過將服務分配給一個服務器 worker 池來充當負載均衡器,這些 worker 能夠實現爲進程、線程或者二者的結合。這些 worker 執行負載均衡器分配給他們的請求。你使用 Web 應用程序框架(例如 Flask 或 Django)編寫的應用程序邏輯運行在這些 worker 中。服務器

這種類型的方案對於有多個 CPU 的服務器比較好,由於你能夠將 worker 的數量設置爲 CPU 的數量,這樣你就能均衡地利用你的處理器核心,而單個 Python 進程因爲全局解釋器鎖(GIL)的限制沒法實現這一點。微信

缺點上,上面的示意圖也清楚展現了這種方案的主要侷限。咱們有 5 個客戶端,卻只有 4 個 worker。若是這 5 個客戶端在同一時間都發送請求,那麼負載均衡器會將某一個客戶端以外的全部請求發送到 worker 池,而剩下的請求不得不保留在一個隊列中,等待有 worker 變得可用。網絡

所以,五分之四的請求會當即響應,而剩下的五分之一須要等一下子。服務器優化的一個關鍵就在於選擇適當數量的 worker 來防止或最小化給定預期負載的請求阻塞。併發

一個異步服務器的配置很難畫,可是我盡力而爲:app

這種類型的服務器運行在單個進程中,經過循環控制。這個循環是一個很是有效率的任務管理器和調度器,建立任務來執行由客戶端發送的請求。與長期存在的服務器 worker 不一樣,異步任務是由循環建立,用來處理某個特定的請求,當那個請求完成時,該任務也會被銷燬。任什麼時候候,一臺異步服務器都會有上百或上千個活躍的任務,它們都在循環的管理下執行本身的工做負載均衡

你可能想知道異步任務之間的並行是如何實現的。這就是有趣的部分,由於一個異步應用程序經過惟一的協同多任務處理來實現這點。這意味着什麼?

當一個任務須要等待一個外部事件(例如,一個數據庫服務器的響應)時,不會像一個同步的 worker 那樣等待,而是會告訴循環,它須要等待什麼,而後將控制權返回給它。循環就可以在這個任務被數據庫阻塞的時候發現另一個準備就緒的任務。最終,數據庫將發送一個響應,而那時循環會認爲第一個的任務已經準備好再次運行,並將儘快恢復它。

異步任務暫停和恢復執行的這種能力可能在抽象上很難理解。爲了幫你應用到你已經知道的東西,能夠考慮在 Python 中使用awaityield關鍵字這一方法來實現,但你以後會發現,這並非惟一實現異步任務的方法。

一個異步應用程序徹底運行在單個進程或線程中,這能夠說是使人吃驚的。固然,這種類型的併發須要遵循一些規則,所以,你不能讓一個任務佔用 CPU 太長時間,不然,剩餘的任務會被阻塞。爲了異步執行,全部的任務須要定時主動暫停並將控制權返還給循環。

爲了從異步方式獲益,一個應用程序須要有常常被 I/O 阻塞的任務,而且沒有太多 CPU 工做。Web 應用程序一般很是適合,特別是當它們須要處理大量客戶端請求時。

在使用一個異步服務器時,爲了最大化多 CPU 的利用率,一般須要建立一個混合方案,增長一個負載均衡器並在每一個 CPU 上運行一個異步服務器,以下圖所示:

Python異步的2種方法

我敢確定,你知道要在 Python 中寫一個異步應用程序,你可使用 asyncio package,這個包是在協程的基礎上實現了全部異步應用程序都須要的暫停和恢復特性。其中yield關鍵字,以及更新的asyncawait都是asyncio構建異步能力的基礎。

https://docs.python.org/3/library/asyncio.html

Python 生態系統中還有其它基於協程的異步方案,例如 Trio 和 Curio。還有 Twisted,它是全部協程框架中最古老的,甚至出現得比asyncio都要早。

若是你對編寫異步 Web 應用程序感興趣,有許多基於協程的異步框架能夠選擇,包括 aiohttp、sanic、FastAPITornado

不少人不知道的是,協程只是 Python 中編寫異步代碼的兩種方法之一。第二種方法是基於一個叫作 greenlet 的庫,你能夠用 pip 安裝它。Greenlets 和協程相似,它們也容許一個 Python 函數暫停執行並稍後恢復,可是它們實現這點的方式徹底不一樣,這意味着 Python 中的異步生態系統分紅兩大類。

協程與 greenlets 之間針對異步開發最有意思的區別是,前者須要 Python 語言特定的關鍵字和特性才能工做,然後者並不須要。個人意思是,基於協程的應用程序須要使用一種特定的語法來書寫,而基於 greenlet 的應用程序看起來幾乎和普通 Python 代碼同樣。這很是酷,由於在某些狀況下,這讓同步代碼能夠被異步執行,這是諸如asyncio之類的基於協程的方案作不到的。

那麼在 greenlet 方面,跟asyncio對等的庫有哪些?我知道 3 個基於 greenlet 的異步包:Gevent、Eventlet 和 Meinheld,儘管最後一個更像是一個 Web 服務器而不是一個通用的異步庫。它們都有本身的異步循環實現,並且它們都提供了一個有趣的「monkey-patching」功能,取代了 Python 標準庫中的阻塞函數,例如那些執行網絡和線程的函數,並基於 greenlets 實現了等效的非阻塞版本。若是你有一些同步代碼想要異步運行,這些包會對你有所幫助。

據我所知,惟一明確支持greenlet 的 Web 框架只有 Flask。這個框架會自動監測,當你想要運行在一個 greenlet Web 服務器上時,它會自我進行相應調整,而無需進行任何配置。這麼作時,你須要注意不要調用阻塞函數,或者,若是你要調用阻塞函數,最好用猴子補丁來「修復」那些阻塞函數。

可是,Flask 並非惟一受益於 greenlets 的框架。其它 Web 框架,例如 DjangoBottle,雖然沒有 greenlets,但也能夠經過結合一個 greenlet Web 服務器並使用 monkey-patching修復阻塞函數的方式來異步運行。

異步比同步更快嗎?

對於同步和異步應用程序的性能,存在着一個普遍的誤解——異步應用程序比同步應用程序快得多。

對此,我須要澄清一下。不管是用同步方式寫,仍是用異步方式寫,Python 代碼運行速度是幾乎相同的。除了代碼,有兩個因素可以影響一個併發應用程序的性能:上下文切換和可擴展性。

上下文切換

在全部運行的任務間公平地共享 CPU 所需的工做,稱爲上下文切換,可以影響應用程序的性能。對同步應用程序來講,這項工做是由操做系統完成的,並且基本上是一個黑箱,不須要配置或微調選項。對異步應用程序來講,上下文切換是由循環完成的。

默認的循環實現由asyncio提供,是用 Python 編寫的,效率不是很高。而 uvloop 包提供了一個備選的循環方案,其中部分代碼是用 C 編寫的來實現更好的性能。Gevent 和 Meinheld 所使用的事件循環也是用 C 編寫的。Eventlet 用的是 Python 編寫的循環。

高度優化的異步循環比操做系統在進行上下文切換方面更有效率,但根據個人經驗,要想看到實際的效率提高,你運行的併發量必須很是大。對於大部分應用程序,我不認爲同步和異步上下文切換之間的性能差距有多明顯。

擴展性

我認爲異步更快這個神話的來源是,異步應用程序一般會更有效地使用 CPU、能更好地進行擴展而且擴展方式比同步更靈活。

若是上面示意圖中的同步服務器同時收到 100 個請求,想一下會發生什麼。這個服務器同時最多隻能處理 4 個請求,所以大部分請求會停留在一個隊列中等待,直到它們被分配一個 worker。

與之造成對比的是,異步服務器會當即建立 100 個任務(或者使用混合模式的話,在 4 個異步 worker 上每一個建立 25 個任務)。使用異步服務器,全部請求都會當即開始處理而不用等待(儘管公平地說,這種方案也還會有其它瓶頸會減慢速度,例如對活躍的數據庫鏈接的限制)。

若是這 100 個任務主要使用 CPU,那麼同步和異步方案會有類似的性能,由於每一個 CPU 運行的速度是固定的,Python 執行代碼的速度老是相同的,應用程序要完成的工做也是相同的。可是,若是這些任務須要作不少 I/O 操做,那麼同步服務器只能處理 4 個併發請求而不能實現 CPU 的高利用率。而另外一方面,異步服務器會更好地保持 CPU 繁忙,由於它是並行地運行全部這 100 個請求。

你可能會想,爲何你不能運行 100 個同步 worker,那樣,這兩個服務器就會有相同的併發能力。要注意,每一個 worker 須要本身的 Python 解釋器以及與之相關聯的全部資源,再加上一份單獨的應用程序拷貝及其資源。你的服務器和應用程序的大小將決定你能夠運行多少個 worker 實例,但一般這個數字不會很大。另外一方面,異步任務很是輕量,都運行在單個 worker 進程的上下文中,所以具備明顯優點。

綜上所述,只有以下場景時,咱們能夠說異步可能比同步快

  • 存在高負載(沒有高負載,訪問的高併發性就沒有優點)
  • 任務是 I/O 綁定的(若是任務是 CPU 綁定的,那麼超過 CPU 數目的併發並無幫助)
  • 你查看單位時間內的平均請求處理數。若是你查看單個請求的處理時間,你不會看到有很大差異, 甚至異步可能更慢,由於異步有更多併發的任務在爭奪 CPU。

結論

但願本文能解答異步代碼的一些困惑和誤解。我但願你能記住如下兩個關鍵點:

  • 異步應用程序只有在高負載下才會比同步應用程序作得更好
  • 多虧了 greenlets,即便你用通常方式寫代碼並使用 Flask 或 Django 之類的傳統框架,也能從異步中受益。

免責聲明:本文內容來源於網絡,文章版權歸原做者全部,意在傳播相關技術知識&行業趨勢,供你們學習交流,若涉及做品版權問題,請聯繫刪除或受權事宜。

-END-



掃碼添加早小起

1. 回覆「進羣」進入Python技術交流羣

2. 回覆「Python」得到Python技術圖書

3. 回覆「習題」領取Python數據處理200題




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

相關文章
相關標籤/搜索