訊息傳遞(message passing)
訊息傳遞是讓並行執行緒合作的一套截然不同的哲學。它不讓執行緒伸手進同一塊共享記憶體、再用鎖保護它,而是讓每個執行緒保有自己的私有狀態,彼此只透過互相傳送訊息來協調——就像同事們從不編輯一份共享文件,而是互發備註信件。Go 語言使這句口號廣為人知,它完美地道出了精髓:不要藉由共享記憶體來通訊;而要藉由通訊來共享記憶體。沒有可競爭的共享可變狀態,就沒有會忘記的鎖、也沒有會弄錯的臨界區間;正確性來自「誰把什麼送給誰」的紀律。
有幾種著名的風格。在行為者模型(actor model,如 Erlang 或 Akka)中,每個行為者是一個帶信箱的隔離單位;它一次處理一則訊息,並可回覆、孵化新的行為者、或改變自己的行為——因為行為者依序處理它的訊息,它的內部狀態自動免於資料競爭。在通訊循序程序(CSP,Go 背後的風格)中,執行緒透過通道(channel)通訊:傳送者把一個值放上通道、接收者把它取下,而對於無緩衝通道,兩者甚至會同步(傳送會阻塞,直到接收就緒,一次會合)。在底層這些可以建在共享記憶體上,但那套機制被藏在通道或信箱背後,所以程式設計師只就訊息來推理。
訊息傳遞確實消除了最陰險的共享記憶體臭蟲(資料競爭、忘記的鎖),並能漂亮地對映到分散式系統——那裡根本沒有共享記憶體,透過網路送訊息是唯一的選項。但它不是免費的午餐,也不總是更快。把資料複製進訊息,比就地碰它更耗成本;用共享結構自然處理的邏輯,當一切都得是訊息時可能變得彆扭;而你是用舊臭蟲換新臭蟲——若兩個行為者各自阻塞等對方的訊息就會死結、若快的傳送者跑贏慢的接收者就會信箱無限膨脹(生產者-消費者問題重現),以及「多方都在意的資料由誰擁有與更新」的難題。它是共享記憶體一個強大、往往更乾淨的替代方案,而非萬用的取代品。
在 Go 裡,工作者與主執行緒不共享任何東西;它們透過通道傳遞結果。主:ch := make(chan int); go worker(ch); result := <-ch。接收 <-ch 會阻塞直到工作者送出,因此不需要鎖,也沒有可競爭的共享變數。
藉由通訊來共享:通道既傳遞了值,也讓兩個 goroutine 同步。
訊息傳遞消除了資料競爭,卻沒消除死結或背壓的問題。兩個互相阻塞等待的行為者照樣死結,而一個對有界通道狂送的快速傳送者照樣撞上生產者-消費者問題。