開發者對並行與並發的誤解
99%的開發者並不了解並行與並發。回想計算機發展的晚期白堊紀時代,我在編程MS DOS,那是一個以所謂的「YOLO模式」運行的操作系統,委婉地說就是沒有規則,可以直接訪問硬體。我記得當時去一家大型科技巨頭面試,被問了一個至今仍讓我心有餘悸的問題。面試官看著我的眼睛說:「嘿,編程結束了,你在履歷上把操作系統列為一項技能。那麼請你給我解釋一下並行和並發的區別。」那一刻,我努力回想在大學裡通過耳濡目染學到的所有操作系統課程內容,那些凌晨2點喝著紅牛臨時抱佛腳的期中考複習時光,但我完全一片空白。用Z世代的話說,就是「我完蛋了嗎?」是的,我完蛋了。我無法回答,面試官讓我像懸空指針一樣掛在那裡,沉默了整整10分鐘。但正是那次經歷,讓我製作了這個視頻。我要確保你們觀看這個視頻的人永遠不必面臨和我一樣的痛苦。
並發與並行的定義與區別
並發
並發是指程序通過交錯處理多個任務的能力。這些任務獨立進展,可能會隨時間暫停和恢復。然而,它們不一定在同一物理瞬間運行。這被稱為交錯執行,其中多個邏輯控制線程共享一個CPU核心。它們通過時間片或自願讓出(我稍後會談到)來實現這一點。
並行
相比之下,並行意味著真正的同時執行,多個任務在不同的CPU核心或處理器上同時運行。
常見混淆點
一個常見的混淆來源是,即使在單一線程或核心的支持下,並發程序也可能看起來像是並行運行。例如,JavaScript中的單線程事件循環可以同時處理多個IO事件,儘管缺乏真正的並行執行。另一方面,使用矩陣乘法訓練的機器學習模型可以在多個核心或GPU上並行運行,利用數據並行性執行實際的同時計算。
總結
簡而言之,雖然並發可以在單個CPU上通過快速切換任務來實現,但真正的並行需要多個處理器或核心。並發對於IO密集型工作負載特別有效,因為程序的大部分時間都花在等待外部資源(如磁盤讀取、網絡響應或數據庫查詢)上。在這些等待期間,除非安排了另一個任務,否則CPU保持閒置。通過並發,多個任務可以共享同一線程或核心。當一個任務因I/O而阻塞時,另一個任務可以取得進展。這導致更好的CPU利用率,而無需更多硬體。
並發示例:使用線程的交錯執行
在CPython中,T1和T2兩個線程默認在同一個CPU核心上運行。Python的全局解釋器鎖(GIL)確保一次只有一個線程執行Python位元組碼。time.sleep
模擬IO延遲。當調用time.sleep
時,活動線程讓出CPU,允許另一個線程運行。這是交錯,而不是真正的同時性。這對於像網絡請求這樣的IO密集型任務非常有效,因為線程可以在等待期間取得進展。請記住,這些任務是並發運行的,而不是並行運行的。它們被調度進出,共享一個核心。
贊助商介紹:Abacus AI的Chat LLM Teams
今天的視頻由Abacus AI贊助。Chat LLM Teams每月每用戶只需10美元,你就可以擺脫對多種昂貴AI工具的雜耍。Chat LLM為你的團隊提供一個中央超級助手,可以立即訪問所有最新的最先进模型、視頻和圖像生成器以及通用編碼代理。我們談論的是GPT4.1、Gemini 2.5和Sonnet 3.7,以及任何新模型一經推出就可以使用,而且你可以用更少的錢獲得更多的使用量,比其他付費服務多約10倍的令牌。Chat LLM直接集成到你的工作流程中,連接到Slack、Microsoft Teams、Google Drive和Confluence。它可以與你的內部文檔交互,分析電子表格中的數據,並成為你團隊自己的知識專家。通過Chat LLM訂閱,你可以獲得兩個改變遊戲規則的工具,Code LLM和Deep Agent。使用Code LLM,你可以提示AI構建新功能。使用Deep Agent,你可以在整個互聯網上進行實時深入研究。生成自定義PDF和演示文稿,甚至根據你的要求開發和部署整個網站或應用程序。如果你有興趣使用這個一體化的超級助手,即Chat LLM,請查看描述和頂置評論中的鏈接。
為什麼並行更適合CPU密集型任務
並行更適合CPU密集型任務,其中性能受到計算本身的限制,而不是等待I/O。例如,壓縮視頻、訓練深度學習模型或計算分形涉及大量可以並行化的數學運算。在這些情況下,將工作分配到CPU核心或使用SIMD或向量指令允許多個指令同時運行。在硬體層面,這是通過多核處理器、向量執行單元和GPU核心實現的。一個實際的例子是視頻編碼器,它將一幀分成塊,並在核心上並行處理它們。另一個例子是神經網絡訓練管道,其中矩陣操作被分派到GPU核心進行真正的同時計算。與有助於隱藏延遲的並發不同,並行通過將工作擴展到獨立的計算單元來提高吞吐量。這種區別在優化性能瓶頸時至關重要。
並行示例:使用多進程的真正同時性
每個進程P1和P2都在自己的Python解釋器中運行,並被分配到不同的CPU核心。任務同時運行,而不僅僅是交錯,因為它們有單獨的內存空間。沒有GIL限制。這些非常適合CPU密集型任務,如數學模擬、加密或渲染。這是真正的並行。任務在不同的核心上同時執行。
任務調度:搶佔式與合作式調度
任務共享執行時間的方式由調度器控制,調度器可以是搶佔式或合作式的。
搶佔式調度
大多數操作系統使用的搶佔式調度,調度器在時間片後強制中斷正在運行的任務,給其他任務一個運行的機會。這實現了時間片並發,確保了響應性和公平性,即使任務不自願讓出。然而,搶佔式切換由於寄存器保存、棧交換和潛在的緩存無效而導致更高的開銷。它還使並行代碼更難以推理,因為存在不可預測的交錯和潛在的競爭條件。
合作式調度
相比之下,合作式調度依賴於自願讓出。任務必須明確地將控制權交還給調度器。這種模型用於許多基於用戶的並行框架,如Python和JavaScript中的asyncio或Go中的Go routines。它具有較低的開銷,因為切換只發生在已知點,並且更容易推理,因為任務切換是確定的。最大的缺點是,如果任務忘記讓出,它可能會阻塞整個系統。這是初學者在異步編程中經常遇到的錯誤。
並行的成本
並行程序必須管理上下文切換,即系統保存一個任務的狀態以讓另一個任務運行。這個過程的開銷是性能的關鍵因素。在傳統的基於線程的系統中,這種切換由操作系統內核管理,可能成本很高。對於每次切換,內核必須將線程的完整執行上下文保存到像線程控制塊(TCB)這樣的數據結構中。這個狀態包括程序計數器(跟踪線程接下來要執行的確切指令)、棧指針(指向線程包含局部變量和函數調用信息的個人棧)、所有CPU寄存器的內容(保存線程的即時工作數據)以及線程的調度狀態和優先級。這個過程需要從用戶模式切換到內核模式再切換回來,這增加了顯著的延遲。在現代基於協程的系統或使用asyncio時,成本大大降低。在這裡,上下文切換完全在用戶空間中進行,由應用程序的運行時或調度器管理,而不是操作系統。需要保存的狀態是最小的,通常只是指令指針(知道從哪裡恢復)和該函數當前使用的特定CPU寄存器。沒有昂貴的內核轉換,使得在數萬個協程之間切換比在線程之間切換效率高得多。
推薦學習平台:Code Crafters
如果你曾經好奇如何從零開始構建Redis、Docker、HTTP服務器、編譯器或DNS服務器,我強烈建議你查看Code Crafters,在那裡你可以學習如何構建這些複雜的開發人員工具。這是我絕對最喜歡的基於項目的學習平台,支持超過20種不同的語言,包括我最喜歡的語言Go。你可以查看描述以獲得所有訂閱40%的折扣。
六月特別活動:Code Crafters的自建shell挑戰
我很高興地宣布,今年六月我們的社區將有一個特別的機會。我們將與Code Crafters合作舉辦一個自建shell挑戰。整個六月,你都可以免費參加這個挑戰。我們將有自己的編程gopher社區排行榜來跟踪我們的進度,我們將為前幾名完成者提供特別獎品,外加三個月的Code Crafters免費訪問權。這次比賽的獎品是AirPods Pro 2、Aura Ring 4 Black或Keyron Q1 Pro。此外,我們社區中在六月完成任何Code Crafters挑戰的任何人都將獲得五次額外的Logitech MX機械組合贈品抽獎機會。所以,如果你準備好構建,請查看描述中的鏈接。
結語
如果你在這個視頻中學到了新東西,請點贊並訂閱以獲取更多未來的內容。如果你想進一步支持頻道,我也在描述中留下了我的Patreon鏈接。一如既往,非常感謝你收看今天的視頻,祝你編程愉快。