第 7 / 11 部 待錄

並行

《The Pragmatic Programmer 務實的程式設計師》Podcast 準備稿:並行

書名: 務實的程式設計師 The Pragmatic Programmer(Your Journey to Mastery, 20th Anniversary Edition) 作者: David Thomas & Andrew Hunt 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|The Pragmatic Programmer.共享狀態就是錯的狀態:Actor 與黑板 (7/11) 涵蓋範圍: 第6章 並行(Topic 33-36:打破時間耦合、不要共用狀態、參與者與程序、黑板)

背景速覽

並行是二十週年版才大幅擴充的主題——1999 初版時多核還不普及,2019 已經「幾乎不可能寫出沒有並行面向的 code」。第六章先把名詞釐清:並行(concurrency) 是「看起來像」同時在跑(靠 fiber/thread/process 切換),平行(parallelism) 是「真的」同時在跑(靠多核/多機硬體)。這章承接第五章解耦的精神,把它推到「時間」這個常被忽略的維度——當各部分能獨立運作,下一步就是讓它們安全地同時跑。核心立場很鮮明:並行的麻煩幾乎全來自共享狀態,所以最好的並行就是「不共享」。

一句話重點

並行最大的麻煩來源是共享狀態(Tip 57「共享狀態就是不正確的狀態」),因為沒有任何程序能保證它看到的狀態還是一致的;務實程式設計師的答案不是更努力地加鎖,而是用 Actor(各自私有狀態、只靠訊息溝通、天生一次處理一則)與黑板(獨立代理人在共享空間自由放任地協作)這類「不共享」的模式,從根本避開鎖的地獄。

值得討論的重點

1. 打破時間耦合:時間是被忽略的設計維度

Tip 56「Analyze Workflow to Improve Concurrency」。時間耦合(temporal coupling) 指程式碼強加了不必要的順序限制——方法 A 必須在 B 之前呼叫、一次只能跑一份報表、tick 一定要在 tock 之前。我們設計時思維天生線性(先做這個、然後做那個),於是不知不覺把順序焊死了。打破它的方法是用活動圖分析工作流程:找出哪些事「可以」同時發生、哪些「必須」依序。書裡用 pina colada 調酒機當例子——12 個看似串列的步驟,分析後發現頂層任務(開果汁機、倒材料、量蘭姆酒、拿杯子、拿傘)其實可以全部同時開始。設計並行時真正要找的,是需要等待但不佔用 CPU 的活動(查資料庫、呼叫外部服務、等使用者輸入)——這些閒等的時間,就是去做別的有用事情的機會。

2. 共享狀態就是不正確的狀態:那塊最後的派

Tip 57,整章的核心金句。書裡的故事絕妙:你在餐廳問服務生還有沒有蘋果派,他看了展示櫃說「有,剩一塊」,你點了;同時餐廳另一頭另一位客人也問同樣的問題,她的服務生也看到「剩一塊」,她也點了——注定有一個人要失望。把展示櫃換成共同銀行帳戶、服務生換成兩台 POS,就是經典的競態條件。問題不在「兩個程序能寫同一塊記憶體」,而在於沒有任何程序能保證它看到的狀態是一致的——服務生把「派數量=1」複製進自己腦中那一刻,這個值就可能已經過時了。根因是「讀取值」和「更新值」不是原子操作。傳統解法是信號量(semaphore,一次只能一人持有,餐廳用「放在派櫃上的塑膠小矮人」當鎖)與互斥鎖(mutex),但作者隨即點出:它們本身也充滿陷阱(死鎖、忘記解鎖、鎖的範圍)。配 Tip 58「隨機失敗通常是並行問題」——那些重現不了、偶爾才出現一次的詭異 bug,很多是競態條件。

3. Actor 與程序:不共享,所以不必鎖

Tip 59「Use Actors For Concurrency Without Shared State」。Actor 是一個獨立的虛擬處理器,有自己私有的本地狀態和一個信箱;信箱來訊息且 actor 閒置時就醒來、處理完一則再處理下一則。關鍵在於它沒有什麼:沒有單一控制者(沒人在排程協調)、狀態只存在訊息和本地狀態裡(外部讀不到)、所有訊息單向(要回應就在訊息裡附上自己的信箱地址)、一次只處理一則訊息到完成。所以 actor 是並行的、非同步的、而且什麼都不共享。書用餐廳場景(顧客、服務生、派櫃三個 actor)示範——最關鍵的觀察是:整段設計裡完全沒有任何並行控制的程式碼,沒有鎖、沒有信號量、沒有同步機制,但它完美處理並行——因為每個 actor 天生一次只處理一則訊息,派櫃永遠不會出現兩個服務生同時拿派的競態。這就是把「不共享狀態」做進架構的威力。

4. 黑板:自由放任的協作

Tip 60「Use Blackboards to Coordinate Workflow」。黑板模式的隱喻是偵探辦案——總督察在會議室架一面大黑板寫上「Humpty Dumpty:意外?謀殺?」,每個偵探(不同班次、不同專業、彼此不必認識)各自把事實、證詞、法醫證據貼上去,看到關聯就再貼推論,直到案子破。這是自由放任式(laissez faire) 的並行:獨立的代理人,有的往黑板放事實、有的取走處理再放回更多資訊,黑板逐漸幫他們得出結論。關鍵特性是徹底解耦——代理人不需要知道彼此存在、可隨時加入離開、黑板能放任何東西、到達順序無所謂。書用房貸申請當實例(聯邦/州/地方法規極繁雜、資料以任意順序非同步到達、有的依賴別的),黑板搭配規則引擎優雅地處理了這種混亂:一個事實被貼上去就觸發適用的規則,規則的輸出再貼回黑板觸發更多規則。歷史源頭是 David Gelernter 的 Linda(typed tuples、模式比對),後來有 JavaSpaces、T Spaces。

注意事項

⚠️ 別把這章講成「鎖是邪惡的、永遠不要用 mutex」。作者的立場更務實:鎖很難用對(死鎖、忘記釋放、粒度錯誤),所以首選是設計成不共享狀態(Actor/黑板);但在必須共享的場景,信號量/互斥鎖仍是工具,只是要清楚它們的陷阱。重點是「優先消除共享」,不是「禁用鎖」。

⚠️ 「並行 vs 平行」這對名詞一定要在開頭講清楚,否則整集容易混亂。一句話記法:並行是「軟體的事」(看起來同時,靠切換),平行是「硬體的事」(真的同時,靠多核)。Actor 模型本身是並行抽象,跑在多核上才順便獲得平行。

專家補充

💡 Actor 模型不是新東西——Carl Hewitt 1973 年就提出了,但它真正發光是因為 Erlang(電信業、要求極高可用性)把它做進語言核心,後來 Akka(JVM)、Elixir、甚至 Pony 都採用。Actor 與第四章「Let it crash」是同一個生態的兩面:actor 各自獨立 + supervisor 管理失敗 = 高韌性並行系統。本書把這條 1970 年代的學術線,講成所有現代開發者都該懂的常識。

💡 「共享狀態就是不正確的狀態」這條,其實是函數式編程「不可變性(immutability)」流行的根本理由。如果狀態不可變,就沒有「讀到過時值」的問題,競態自然消失——這是 Clojure、Elixir、以及 React 的不可變資料流背後的同一個直覺。可以接到「為什麼這十年函數式思想回潮」這個更大的產業趨勢。

💡 黑板模式雖然在企業開發少見,但它的精神在現代大行其道:訊息佇列(Kafka、RabbitMQ)、事件溯源(event sourcing)、甚至區塊鏈的共享帳本,都是「獨立參與者在共享空間自由協作、靠資料而非直接呼叫溝通」的變體。本書 1999 年押的 Linda/JavaSpaces 沒紅,但它賭的「解耦協作」方向完全押中了。

討論問題

🎙️ 開場鉤子:「最難抓的那種 bug——你重現不了、它一個月才出現一次、QA 永遠複製不出來——很多時候根本不是你的邏輯錯了,而是兩段 code 在搶同一塊狀態。這集講的就是並行的萬惡之源:共享狀態。」

🎙️ 自問自答:「對付並行,我們是不是該更努力地加鎖?」——本書的答案相反:鎖是最難用對的工具之一,與其加更多鎖,不如從架構上讓各部分根本不共享狀態。Actor 一次只處理一則訊息、黑板靠資料協作,這些模式讓你寫並行程式時,竟然一行鎖都不用寫。

🎙️ 帶走的一題:「想一個你系統裡『會被多方同時讀寫』的狀態——它真的需要被共享嗎?如果把它改成『誰擁有就只有誰能改、別人想動就發訊息給它』,會變簡單還是變複雜?」

更大範圍關聯

  • 接續解耦:本章是 EP6「彎曲或弄壞」在「時間維度」的延伸——解耦處理「依賴」,並行處理「時間與順序」。打破時間耦合 = 解耦的時間版。
  • 與韌性工程:Actor + supervisor + Let-it-crash(EP5)構成一整套高可用並行哲學,接 Erlang/OTP、Akka、《Release It!》。本書是把這套電信級可靠性思想普及給一般開發者的橋。
  • 與函數式回潮:「共享狀態即不正確狀態」是不可變性與函數式編程流行的根本論據,接 Clojure、Elixir、React 的不可變資料流。
  • 與三經典:並行是三經典裡 Pragmatic 獨家深入的領域(Clean Code、Refactoring 著墨少)——這也是本書「持續重新平衡知識組合」的證明:作者在 2019 版補上了 1999 年還不急迫、如今已是必備的主題。

錄製建議

  • 建議時長:約 23-25 分鐘。配比:並行 vs 平行釐清 + 時間耦合約 6 分(pina colada 好講)、共享狀態約 7 分(蘋果派的故事是全集高潮,慢慢演)、Actor 約 7 分(餐廳三 actor「一行鎖都不用」是亮點)、黑板約 4 分(偵探隱喻收尾)。
  • 討論策略:用「共享狀態是萬惡之源 → 所以最好別共享」這條主線貫穿。蘋果派故事先建立「為什麼難」,Actor 與黑板再給「怎麼從架構上繞過去」。把「Actor = Erlang 韌性 + 函數式不可變回潮」這條現代脈絡帶進去,讓並行不只是技術細節而是一個思想趨勢。
  • 提醒聽眾:辨識並行機會(活動圖)是容易的部分,安全實作才是難的——這章的價值在於告訴你「不共享」是繞過難題的捷徑。結尾預告 EP8「當您寫程式時(上)」——回到鍵盤前最日常的決策:聆聽直覺、別靠巧合寫程式、何時重構。