返回文章列表

智慧工作或增加資源:解析併發與平行的核心差異

本文透過酒吧調酒師案例,生動地區分了「併發」(更智慧地工作)與「平行」(投入更多資源)的核心差異。文章深入探討併發在處理 I/O 操作與維持使用者介面響應性的應用,並強調理解「參考框架」(如程式設計師、作業系統視角)是釐清混淆概念的關鍵。最終將異步程式設計定義為一種高階抽象,它讓開發者能有效利用作業系統提供的多工與執行緒等底層併發機制,以更簡潔的方式實現高效能的任務執行。

系統架構 電腦科學

在現代軟體架構中,處理效能瓶頸與提升資源利用率是核心課題。隨著多核心處理器普及與網路應用服務的複雜化,單純的同步執行模型已難以滿足需求。本文從根本上剖析「併發」與「平行」這兩個經常被混淆的概念,闡明前者旨在智慧地管理任務切換以減少等待,後者則透過增加運算單元來同時處理任務。理論的釐清關鍵在於建立正確的「參考框架」,理解從程式設計師到作業系統的不同視角。文章將探討作業系統如何透過搶佔式多工實現底層併發,並說明異步程式設計如何作為高階抽象,讓我們能更優雅地駕馭這些複雜機制,打造高響應與高吞吐量的應用程式。

替代方案三:單一調酒師的異步任務執行

回到最初的起點,我們需要尋找一種更智慧的工作方式,而非僅僅增加資源。你要求調酒師在啤酒靜置期間,開始接新的訂單,確保他們在有顧客時絕不閒置等待。開業當晚的結果令人驚訝!在調酒師連續忙碌數小時後,你計算出每個訂單的平均處理時間僅略高於20秒。幾乎所有的等待時間都被消除了。

理論上,每小時的吞吐量可達240杯啤酒。如果再增加一位調酒師,總吞吐量將超越12位調酒師的同步處理能力。

然而,你意識到實際每小時並未達到240杯,因為訂單的到來並非均勻分佈。有時,調酒師正忙於處理新訂單,導致已經靜置完成的啤酒無法立即被加滿並上菜。在現實情況下,每小時的實際吞吐量約為180杯啤酒。

儘管如此,兩位調酒師以這種方式工作,每小時仍可服務360杯啤酒,這與雇用12位調酒師所能達到的數量相同。這已經很不錯了,但你開始思考是否能做得更好。

替代方案四:兩位調酒師的平行與異步任務執行

如果雇用兩位調酒師,並讓他們按照方案三的異步方式工作,但增加一個關鍵改變:允許他們互相竊取任務。例如,調酒師1可以開始倒酒並將其靜置,如果此時調酒師1正忙於倒新的訂單,那麼調酒師2就可以接手,將靜置完成的啤酒加滿並上菜。

這樣一來,很少會出現兩位調酒師同時忙碌的情況,因為其中一杯正在處理中的啤酒已準備好加滿並上菜。幾乎所有的訂單都能在最短的時間內完成並上菜,讓顧客更快地拿到啤酒,並為新顧客騰出空間。

透過這種方式,你可以進一步提高吞吐量。雖然仍無法達到理論最大值,但會非常接近。開業當晚,你發現每位調酒師每小時處理230個訂單,總吞吐量達到每小時460杯啤酒。

收入可觀,顧客滿意,成本降到最低,你成為了這個世界上最奇特(但極其高效)的酒吧的快樂經理。

核心啟示

併發是關於更智慧地工作平行是關於投入更多資源來解決問題。

併發與I/O的關係

從上述案例中,我們不難理解,編寫異步程式碼主要在於需要智慧地利用資源,以達到最佳效能。

如果一個程式正在努力解決一個計算密集型問題,那麼併發可能幫助不大。這時,平行就派上用場了,因為如果問題可以分解成多個可平行處理的部分,那麼投入更多資源就能有效解決問題。

考慮併發的兩種主要應用場景:

  1. 當執行I/O操作時,需要等待某些外部事件發生。
  2. 當需要分散注意力,防止一個任務等待過久。

第一種是典型的I/O案例:你必須等待網路呼叫、資料庫查詢或其他外部事件完成,才能推進當前任務。然而,你有許多任務需要處理,因此,與其等待,不如繼續處理其他任務,並定期檢查該任務是否準備好推進,或者確保在任務準備好時收到通知。

此圖示將透過酒吧案例,視覺化地呈現異步處理如何提升效率,以及併發與平行在不同情境下的應用。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

actor "顧客" as Customer
entity "訂單" as Order
entity "Guinness啤酒" as Beer

rectangle "調酒師 (CPU核心)" {
component "倒酒 (15s)" as Pour
component "靜置 (100s)" as Settle
component "加滿 (5s)" as Fill
component "上菜" as Serve
}

rectangle "方案三:單一調酒師 (異步)" {
Pour -[#blue]-> Settle : 啟動靜置 (非阻塞)
Settle --> Order : 接新訂單 (同時進行)
Order --> Pour : 處理新訂單
Settle -[#green]-> Fill : 靜置完成 (事件驅動)
Fill -[#green]-> Serve : 上菜
}

rectangle "方案四:兩位調酒師 (平行+異步)" {
component "調酒師A" as BarA
component "調酒師B" as BarB
BarA -[#red]-> Pour : 訂單1 (啟動靜置)
BarB -[#red]-> Order : 接新訂單 (同時)
BarA -[#red]-> Order : 接新訂單 (同時)
BarB -[#red]-> Fill : 訂單1 (靜置完成後接手)
BarA -[#red]-> Serve : 訂單2 (靜置完成後接手)
}

Customer --> Order : 點餐
Order --> Beer : 製作
Beer --> Serve : 上菜

@enduml

看圖說話:

此圖示進一步展示了酒吧案例中,異步處理如何顯著提升效率。在方案三:單一調酒師(異步)中,調酒師在倒酒後啟動靜置過程,但並非等待,而是立即轉去接新訂單處理新訂單。當靜置完成後,一個事件會觸發調酒師回頭加滿上菜。這種模式極大地減少了調酒師的閒置時間,提升了單一資源的利用率。

方案四:兩位調酒師(平行+異步)則結合了平行的資源增加和異步的智慧利用。兩位調酒師可以同時接新訂單。當一位調酒師啟動了某個訂單的靜置過程後,另一位調酒師可以接手處理其他訂單,甚至接手完成前一個調酒師啟動的靜置完成的訂單。這種任務竊取機制,使得兩位調酒師的協同工作效率達到極致,大幅提高了整體吞吐量。這個案例生動地說明了併發的核心價值:透過智慧地管理等待時間,最大限度地利用有限資源,從而實現更高的效率和吞吐量。

玄貓認為,程式設計師對於參考框架的理解,是區分各種多工概念的關鍵。沒有明確的參考框架,許多看似矛盾的觀點便會浮現,導致混淆。

處理使用者介面與CPU密集型任務的併發

第二種併發應用場景,常見於具有使用者介面(UI)的應用程式。假設你只有一個核心,如何防止在執行其他CPU密集型任務時,整個UI變得無響應?

解決方案是:你可以每隔16毫秒停止當前正在執行的任務,運行更新UI的任務,然後再恢復之前的工作。這樣一來,你每秒需要停止/恢復任務約60次,但你將擁有一個完全響應的UI,其刷新率大約為60赫茲。

作業系統提供的執行緒

我們將在本書後續章節中更深入地探討作業系統執行緒在處理I/O方面的策略,但玄貓在此也將其納入討論。使用作業系統執行緒來理解併發的一個挑戰是,它們似乎被直接映射到核心。然而,這並非一個完全正確的心智模型,儘管大多數作業系統會盡力將一個執行緒映射到一個核心,直到執行緒數量達到核心數量為止。

一旦我們創建的執行緒數量超過了核心數量,作業系統就會透過其排程器在我們的執行緒之間切換,並讓每個執行緒併發地推進。此外,我們必須考慮到,我們的程式並非系統上唯一運行的程式。其他程式也可能產生多個執行緒,這意味著CPU上的執行緒數量將遠多於核心數量。

因此,執行緒既可以是實現平行的手段,也可以是實現併發的手段。這引導玄貓思考,併發需要被定義在某種參考框架中。

選擇正確的參考框架

當你編寫的程式碼從你的角度來看是完全同步的時候,請停下來思考一下,從作業系統的角度來看,它又是如何運作的。

作業系統可能根本不會從頭到尾運行你的程式碼。它可能會多次停止和恢復你的行程。CPU可能會被中斷,並處理一些輸入,而你卻認為它只專注於你的任務。

所以,同步執行只是一種錯覺。但從你作為程式設計師的角度來看,它並非錯覺,這是一個重要的啟示:

當我們在沒有提供任何其他上下文的情況下談論併發時,我們是以你作為程式設計師和**你的程式碼(你的行程)**作為參考框架。如果你在思考併發時沒有將這一點牢記在心,你很快就會感到困惑。

玄貓之所以花費這麼多時間來闡述這一點,是因為一旦你意識到擁有相同定義和相同參考框架的重要性,你就會開始明白,你所聽到和學到的一些看似矛盾的觀點,實際上並非如此。你只需要首先考慮參考框架。

異步與併發的關係

你可能會好奇,既然本書是關於異步程式設計的,為何我們要花這麼多時間討論多工、併發和平行。

主要原因在於,這些概念彼此之間密切相關,甚至可能具有相同(或重疊)的含義,具體取決於它們所使用的上下文。

為了盡可能明確地定義這些術語,玄貓將對它們進行比通常情況下更狹義的定義。然而,請注意,我們無法取悅所有人,我們這樣做是為了讓這個主題更容易理解。另一方面,如果你喜歡激烈的網路辯論,這是一個很好的起點。只需聲稱別人的併發定義是100%錯誤,或者你的定義是100%正確,然後你就可以開始了。

為了本書的目的,我們將堅持以下定義:異步程式設計是程式語言或函式庫抽象併發操作的方式,以及我們作為語言或函式庫的使用者,如何利用這種抽象來併發執行任務

此圖示將展示不同參考框架下的併發視角,以及異步程式設計在其中的定位。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

rectangle "程式設計師視角 (同步錯覺)" as ProgrammerView {
component "程式碼執行順序" as CodeOrder
}

rectangle "作業系統視角 (搶佔式多工)" as OSView {
component "行程調度" as ProcessScheduling
component "執行緒切換" as ThreadSwitching
}

rectangle "CPU視角 (指令執行)" as CPUView {
component "單指令執行" as SingleInstruction
component "硬體中斷處理" as InterruptHandling
component "管線化/亂序執行" as Pipelining
}

rectangle "異步程式設計 (抽象層)" as AsyncProgramming {
component "併發操作抽象" as ConcurrencyAbstraction
component "任務併發執行" as TaskConcurrency
}

ProgrammerView --> OSView : 程式碼被OS中斷
OSView --> CPUView : OS調度CPU資源
CPUView --> ProgrammerView : CPU執行指令

AsyncProgramming --> ProgrammerView : 提供併發抽象
AsyncProgramming --> OSView : 依賴OS底層機制

note "併發定義取決於參考框架" as Note
Note --> ProgrammerView
Note --> OSView
Note --> CPUView

@enduml

看圖說話:

此圖示清晰地呈現了不同參考框架下對程式執行和併發的理解。從程式設計師視角看,程式碼似乎是按照書寫順序同步執行的,這是一種「同步錯覺」。然而,從作業系統視角看,它透過行程調度執行緒切換,頻繁地中斷和恢復程式的執行,實現了搶佔式多工。更底層的CPU視角則揭示了指令的單指令執行硬體中斷處理以及管線化/亂序執行等複雜機制。

異步程式設計作為一個抽象層,其目的在於提供一種方式,讓程式設計師能夠更有效地利用底層的併發能力,實現任務併發執行。它依賴於作業系統提供的底層機制,並將複雜的併發操作抽象化,使得程式設計師能夠在不陷入底層細節的情況下,編寫出高效的併發程式碼。這個圖示強調了理解不同參考框架的重要性,因為這有助於消除對併發概念的混淆,並更精確地定義和應用異步程式設計。

玄貓認為,理解作業系統在程式執行中的核心作用,是掌握高階程式設計概念,特別是異步程式設計的基礎。作業系統不僅是硬體的抽象層,更是管理資源和實現多工的關鍵。

作業系統:抽象與併發的核心

作業系統(OS)已經提供了一種處理併發的現有抽象,即執行緒。使用作業系統執行緒來處理異步問題通常被稱為多執行緒程式設計。為了避免混淆,玄貓不會將直接使用作業系統執行緒稱為異步程式設計,儘管它解決了相同的問題。

鑑於異步程式設計現在被定義為程式語言或函式庫中對併發或平行操作的抽象,因此它在沒有作業系統的嵌入式系統上,與在具有複雜作業系統的目標程式上,都同樣具有相關性。這個定義本身並不意味著任何特定的實作方式,儘管玄貓將在本書中探討一些流行的實作。

如果這聽起來仍然複雜,玄貓理解。僅僅坐著反思併發是困難的,但如果我們在編寫異步程式碼時將這些想法牢記在心,玄貓保證它會越來越不那麼令人困惑。

作業系統的角色

作業系統是我們作為程式設計師所做一切的核心(除非你正在編寫作業系統或在嵌入式領域工作)。因此,我們無法在不詳細討論作業系統的情況下,討論任何程式設計的基本原理。

從作業系統角度看併發

這與玄貓之前所說的,併發需要在一個參考框架內討論的觀點相符。玄貓解釋過,作業系統可能隨時停止和啟動你的行程。

我們所稱的同步程式碼,在大多數情況下,只是對我們程式設計師來說看起來是同步的程式碼。無論是作業系統還是CPU,都不是生活在一個完全同步的世界中。

作業系統使用搶佔式多工。只要你運行的作業系統是搶佔式排程行程的,你將無法保證你的程式碼會逐條指令地運行。

值得注意的是,在現代具有4、6、8或12個物理核心的機器上,如果系統負載很低,你的程式碼實際上可能在其中一個CPU上不間斷地執行。這裡的重點是,你無法確定,並且無法保證你的程式碼會不間斷地運行。

與作業系統協同工作

當你發出一個網路請求時,你並不是直接要求CPU或網卡為你做些什麼,你是在要求作業系統為你與網卡通訊。

作為程式設計師,如果沒有發揮作業系統的優勢,你將無法使你的系統達到最佳效率。你基本上無法直接存取硬體。你必須記住,作業系統是硬體之上的一個抽象層。

然而,這也意味著,要從頭到尾理解一切,你還需要了解你的作業系統如何處理這些任務。

為了能夠與作業系統協同工作,你需要知道如何與它通訊,這正是玄貓接下來要探討的內容。

結論

縱觀現代管理者的多元挑戰,我們發現高效率系統設計的真正突破點,並非僅止於技術選型,而是源於對「參考框架」的深刻洞察。許多技術領導者之所以在併發、平行與異步之間感到困惑,其根本瓶頸在於固守單一的「程式設計師視角」,而忽略了作業系統層級「搶佔式多工」的現實。唯有整合這兩種看似矛盾的觀點,將程式碼的邏輯順序與作業系統的資源調度視為一個整體系統來考量,才能真正發揮硬體潛力,從根源上優化效能。

隨著雲端原生與分散式架構成為主流,這種跨越抽象層、洞悉底層運作的系統性思維,將成為區分資深架構師與一般開發者的關鍵能力,其價值遠超過對單一異步框架的熟練掌握。玄貓認為,主動建立這種多重參考框架的心智模型,是技術領導者從「解決問題」邁向「設計優雅系統」的必要修養,也是在複雜技術環境中保持策略清晰度的基石。