pragmaticProgrammer

Concurrency - 並行

千呼萬喚 終於等到Pragmatic Programmer 20週年紀念版 如果沒聽過這本書 你大概也聽過程序員修煉之道︰從小工到專家這本暢銷了20年的書 終於等到了再版

在再版裡面 刪掉了比較過時的內容和範例 收集了20年來收到的feedback 在讓這本書的內容也可以適用於2020年的程序員 但在我細細品嚐後發現 其實很多人生的哲學並不是只適用於程序員 各行各業看了都可以有所收穫

因為每個篇章的篇幅都不長 所以筆記也用條列式紀錄

本篇的圖片以及程式碼來自於原書內容

第六章: 並行

開始這章之前 我們來定義兩個容易搞混的詞

Concurrency: 並行 指的是執行兩個或多個程式片段時 就好像他們同時執行一樣

Parallelism: 平行 指的是他們真的同時執行

如果要實作並行 你必須在同一個環境中 在程式碼的不同部分之間切換執行 通常是使用thread或process的概念實作

如果要實作平行 你需要同時能做兩件事的硬體 也許是一個多核心的CPU 或是多個CPU的電腦 或是很多台電腦連接在一起

本文會先說明為什麼打破時間耦合是高效並行化的必要步驟 然後在不要共用狀態中說明為什麼共用狀態會讓並行窒礙難行 最後再引入參與者模式 介紹如何在不共用資料 而是透過預定義的簡單語意 通過channel進行通訊

打破時間耦合

什麼是時間耦合(temporal coupling)呢 我們很常在寫程式的時候 寫出類似這樣的邏輯

先做A -> 再做B 等等

B要等到A結束之後再做 但其實這種方法不是很彈性 也不太符合現實 會這麼做的原因往往只是我們學寫程式的時候 都是從sequential的程式語言開始學

如果做B這件事不依賴做A 那我們應該要讓並行發生 去掉時間或順序所產生的耦合 這樣我們可以獲的很多彈性 並且減少時間上的依賴

尋找並行

我們要在專案中 找出哪些操作可以同時發生 哪些操作必須嚴格的照順序發生 常見的方法是使用UML中的activity diagram

來看一下酒保怎麼做出飲料

Alt text

看了流程之後 你發現其實步驟 1, 2, 4, 10, 11 可以在一開始就並行的去做 做完1,2,4之後 3,5,6又可以並行的去做 所以要是每個任務的單位時間一樣 原本需要12個單位的時間 如果可以成功並行 那就只需要6個單位時間

並行的機會

雖然我們看到了並行的可能 但並不表示這些地方真的可以如我們所願的並行 剛剛說的6個單位時間的完成法 酒保必須要有5雙手才可以達成

所以在程式碼中 我們想要找出耗費時間的任務 但這任務卻又不是在執行程式碼 比如說查詢資料庫 存取外部服務 等待使用者輸入 等等 這些事情會讓程式停滯 這些停滯的時候就是處理並行的好時機

平行的機會

記住兩者的區別 並行是個軟體機制 平行是個硬體機制

如果我們有多個處理器 我們就可以為他們分配 可以平行拆分的工作 然後再合併結果

要找出並行和平行的機會並不難

找出機會不難 難的是如何安全地實現 本文接下來的章節會討論安全實現的問題

不要共用狀態

假設你在一家餐廳 你問服務生有沒有蘋果派 服務生轉頭看了一下烤箱還有最後一個 於是你就點了蘋果派

但在同一個時間 有另一個客人也問一樣的問題 也點了蘋果派 那這家餐廳就出包了 一定有一個客人吃不到

Shared State Is Incorrect State

非原子性的更新

簡單的來看一下程式碼

Alt text

雖然現實生活中 這兩個服務員是平行工作 因為這兩個服務員幾乎同時執行程式碼 同時認為display_case.pie_count > 0 所以兩個都執行display_case.take_pie()

這裡的問題不是兩個人共用記憶體 問題是出在兩個人無法保證他們對記憶體的看法一致 因為當他們要看剩多少派的時候 需要先把共享記憶體的資料複製到私人記憶體 再來做判斷 但在判斷的時候可能資料已經過時了

那要怎麼確保原子性呢

Semaphor

Semaphor就是個最簡單的鎖 當服務生手上握著鎖的時候 才可以幫忙點餐

case_semaphore.lock()
if display_case.pie_count > 0 
  promise_pie_to_customer() 
  display_case.take_pie() 
  give_pie_to_customer()
end
case_semaphore.unlock()

非交易型更新

共用記憶體身為並行性問題的根源 自然受到了很多關注 事實上 只要是任何應用程式程式碼可共用可變資源 都可能出現問題

Random Failures Are Often Concurrency Issues

除了semaphor 其他隊共用資源的獨佔存取包含mutex, monitor等等

這些需要外在的枷鎖才可以正確存取共用狀態的方法很複雜 也很容易出錯 有沒有簡單一點的方式來寫並行的程式呢??

參與者模型 Actor model

什麼是Actor呢 就是一個獨立的虛擬處理器 有著自己的狀態和郵箱 當你的郵箱有了新的訊息而且參與者有空的話 就會處理這個訊息 一路處理到郵箱的所有訊息處理完後 又進入空閒狀態

關於Actor 有幾個重要的特質

1.系統內沒有東西是可控的 沒有人能安排接下來要發生什麼 也沒有人可以安排從原始資料到最終輸出的資訊傳輸

2.系統的唯一狀態被保存在訊息之中 還有每個參與者的本地狀態

3.除了接收端可以檢查訊息之外 沒有人可以看到參與者收到的訊息 除了參與者之外 也沒有人可以存取參與者的本地狀態

4.所有的資訊都是單向的 沒有回覆的概念 如果你希望參與者回應 你就要在訊息裡面寫好自己的郵箱地址 然後參與者會再發另一條消息到指定的郵箱

5.參與者處理每條訊息直到完成 並且一次只處理一條訊息

因此 參與者的執行是並行 非同步 而且不共用任何內容的

使用參與者模型可以做到不共用狀態的並行工作

實作參與者模型的語言

Erlang/Elixir 有興趣的話可以看他們怎麼有效的使用參與者模型 增加應用的可靠程度