本文為翻譯作品,若您具備一定的英語閱讀水平,建議閱讀原文。
此文所需閱讀時間,預計8分鐘。
這是關於非同步編程的系列文章的第三篇文章。整個系列嘗試回答一個簡單的問題:「什麼是非同步?」。
當我剛開始深入研究這個問題時,年輕的我以為我知道它是什麼。事實證明,年輕的我並無法完整的陳述何為非同步編程。時過境遷,現在讓我們一起討論一下!
整個系列:
一些應用程序實現並行,採用多進程來代替多線程(第一篇)。在這篇文章中,我將就線程實現來展開,但你可以很輕鬆地將其切換到進程。因為儘管一些程序細節存在不同,但本文涉及的模型,在概念上對於兩者是一致的。
同時此文中,我們將只談論明確的合作型多任務處理機制--回調(callbacks),因為這是非同步框架實現中最廣泛,最常用的選項。
現代應用程序最常見的活動是處理輸入和輸出,而不是大規模的數字運算。使用輸入/輸出(I/O)功能的問題在於它們是阻塞的。與CPU的計算速度相比,硬碟的寫入和網路的讀取操作需要非常長的時間。在這些任務完成之前,應用的功能不會結束,在此期間,你的應用程序並不會進行任何操作。對性能有高要求的應用程序而言,這是主要的瓶頸,因為其他活動和其他I / O操作都在等待。
標準解決方案之一是使用線程。每個阻塞I/O操作都在一個單獨的線程中啟動。當一個線程在執行阻塞功能時,處理器可以調度另一個線程去執行其餘任務,這樣做的話實際上需要CPU。
在這篇文章中,我們將大致討論同步和非同步的概念。
在同步概念中,單個線程被分配給單個任務並開始處理它。當一個任務完成後,線程執行下一個任務並進行相同的操作:它一個接一個地執行所有命令以完成一個指定的任務。在這個系統中,線程不能中途離開任務並繼續下一個任務。因此,我們可以確定:無論何時何地執行某個功能 - 它都無法設置為保持狀態,並且將在完整結束一個任務才開始執行另一個功能(可以利用當前功能的作用,改變數據)。
Single-threaded(單線程)
如果系統是單線程執行的,並且有幾個任務與之相關,那麼它們將一個接一個地順序執行。
如果任務總是按照指定的順序執行,在邏輯上做一個明確的簡化:假設後續任務執行時,所有前期任務都已完成,沒有錯誤發生,所有輸出都可供使用。
如果其中一個命令很慢,整個系統將等待此命令完成 -- 單線程執行沒有辦法繞開此問題。
Multi-threaded(多線程)
在多線程系統中,上面提到的原則得以保留--單個線程被分配給單個任務並且此線程執行任務直到任務完成。
但是在此係統中,每個任務都在一個獨立的控制線程中執行。這個控制線程可能由操作系統管理,可能在具有多個處理器或多核的系統上,並行運行,也可能在單個處理器上交錯運行。
那麼現在我們有多個線程,同時有多個任務(不是一項任務,而是幾項不同的任務)可以並行執行。通常,任務需要的處理時間是不同的,事實上,已經完成其中一個任務的線程可以轉到下一個。
多線程程序更複雜,也更容易出錯。常見的麻煩有:競爭條件(race-condition),死鎖(dead-locks)和資源匱乏(resource starvation)。
其他實現採用另外一種風格--非同步,無阻塞風格。非同步是一種並發編程的風格,但不是並行。
大多數現代操作系統都提供事件通知子系統(event notification subsystems)。例如,在介面上調用普通的讀取操作(read)將阻塞,直到發送方真正發送了一些內容。反之,應用程序可以請求操作系統監聽介面,並將一個事件通知放入特定隊列中。應用程序可以方便地檢查事件(可能為了高效使用處理器,會提前進行一些數值運算操作)同時提取數據。它是非同步的,因為應用程序在某一點表達興趣,然後在另一點使用數據(在時間維度和空間維度)(原文:the application expressed interest at one point, then used the data at another point (in time and space))。它是非阻塞的,因為應用程序的線程是自由,可以執行其他任務。
read
非同步代碼從主應用程序線程中刪除阻塞操作,以便於可以持續執行,但是在時間上稍等一會(或者可能在空間上的其他地方),處理程序可以進一步處理阻塞操作。簡單來說,主線程設置任務並將其執行轉移到以後的某個時段(或另一個獨立的線程)。
雖然非同步編程可以預防上面提到的問題(競爭條件,死鎖和資源匱乏),但實際上,它是針對一個完全不同的問題而設計的:CPU上下文切換。當您同時運行多個線程時,每個CPU內核仍然只能一次運行一個線程。為了允許所有線程/進程共享資源,CPU經常切換上下文。CPU設計為了精簡邏輯,會間隔一段隨機時間,保存一個線程的所有上下文信息並切換到另一個線程。意味著CPU會以非確定的間隔在你的線程之間不斷切換。線程也是資源,它們不是免費的。
非同步編程本質上是帶有用戶空間線程的合作型多任務處理機制,應用程序在用戶空間線程中管理線程和上下文切換,而不是CPU。基本上,在非同步世界中,上下文切換僅發生在定義好的切換點,而不是非確定性的時間間隔。
對比同步模型,非同步模型在以下情況表現更好:
這些情況幾乎完美的展示了在客戶端--伺服器環境下,繁忙伺服器(例如web伺服器)的經典特徵。每個任務以接收請求和發送回復的形式,表示一個帶有I/O操作的客戶端請求。伺服器實現是非同步模型的主要候選項,這就是Twisted和Node.js以及其他非同步伺服器庫,近年來變得如此受歡迎的原因。
為什麼不使用更多的線程呢?如果一個線程在I/O操作上阻塞,另一個線程可以取得進展,對不對?但是,隨著線程數量的增加,您的伺服器可能會遭遇性能問題。對於每個新線程,都需要一些內存開銷--與線程狀態的創建和維護相關聯的內存開銷。非同步模型另一個提高性能的表現是它避免了上下文切換--操作系統每次將控制從一個線程轉移到另一個線程時,它必須保存所有相關的寄存器(registers)、記憶圖(memory map)、堆棧指針(stack pointers)、CPU上下文(CPU context)等等,以便於其他線程可以在中斷的地方繼續執行。這樣做的開銷可能非常大。
如果執行線程忙於處理其他任務,新任務到達的事件如何通知應用程序?實際上,操作系統有許多線程,實際與用戶交互的代碼與我們的應用程序分開執行,只嚮應用程序發送通知。
那麼如何管理所有的事件線程呢?在特定的事件循環中。
事件循環正如它的字面意思一樣,有一個事件隊列(Task Queue)(所有已觸發的事件都存儲在其中--上圖中被稱為任務隊列)和一個循環(Event Loop),此循環不斷地將這些事件從隊列中拉出並執行這些事件的回調(所有的命令都在調用堆棧(Call Stack)上執行)。API表示非同步功能調用的API,例如等待來自客戶端或資料庫的響應。
因此,所有操作首先進入調用堆棧(Call Stack),而非同步指令進入API,等指令動作完成後,其需要的回調操作進入任務隊列(Task Queue)。然後再次在調用堆棧上執行。
此進程的協調(Coordination)發生在事件循環(Event Loop)中。
你看,這與我們上一篇文章中討論的反應器模式有什麼不同?對--完全一致。
當事件循環形成應用程序的中央控制流程結構時,它或許被稱為主循環或主事件循環。這一稱呼是名副其實,因為這樣的事件循環處於應用程序內的最高控制級別。
在事件驅動的編程中,應用程序表達對某些事件的興趣,並在事件發生時對它們做出響應。事件循環負責從操作系統中收集事件或監聽其他事件源,開發者可以註冊在事件發生時需要調用的回調。事件循環通常會一直運行。
JS事件循環概念解釋:
總結一下整個理論系列:
此係列還剩最後一篇文章,是關於Python3.5+中,非同步編程相關概念的實現。由於本人對Python不甚瞭解,對此係列的翻譯到此文為止,感興趣的朋友可以直接閱讀原文。
Asynchronous programming. Python3.5+?luminousmen.com 推薦閱讀: