你剛寫完那行 go counter.Inc(),心里還在竊喜——一千個并發,不過是一行循環的事。壓測一跑,數字穩穩停在 1000,哦不對,998……再跑一次,又是 1001。那個被踩碎的計數器,正靜靜躺在共享內存的坑底,對你豎起一根嘲諷的中指。
每個 Go 開發者遲早都會聽到那句:“別用共享內存來通信,要用通信來共享內存。”起初你會覺得這是某種可愛的格言,就像“少即是多”一樣適合印在馬克杯上。但當你親手維護過高吞吐量的日志管線、親眼見過一個慢日志打印把數百個 goroutine 卡在鎖外頭的時候,這句格言就不再是好聽的口號——它變成了一整套軟件設計的坐標系。
![]()
這個坐標的原點,是 1978 年 Tony Hoare 寫下的論文《通信順序進程》(Communicating Sequential Processes)。Go 的并發模型不是憑空想出來的,它幾乎就是把這篇四十多年前的理論搬進了生產環境。一開始我也只是把 goroutine 看作“輕量線程”,把 channel 看作“線程安全的隊列”——這沒什么不對,但當系統復雜度來了,你會發現這種理解只是故事的前三頁。
共享狀態的問題,往往從最無辜的姿勢開始。多個 worker 需要讀同一份數據,于是你把它往內存里一放,讓 worker 直接讀、直接寫。然后不出意外地出意外了:兩個 goroutine 同時改寫同一個 Counter.value,每次運行的結果都像開盲盒。你加上互斥鎖,完事。但真正的麻煩這時候才剛冒頭——因為你很可能從此把“共享內存+鎖”當成默認架構,整個系統的控制流就慢慢變成了一團繞不開的鎖順序問題。
你會開始琢磨:這塊數據到底歸誰管?誰能改它?這把鎖已經拿了多久?這個函數里面調的下一個函數是不是也需要同一把鎖?并發高了會不會死鎖?為什么 p99 響應時長突然就裂開了?這種痛苦在高吞吐量的網絡服務、容器運行時、內存型存儲里尤其扎心——代碼看起來是安全的,因為到處都有鎖;但“安全”不等于“簡單”,而“簡單”才是生產系統能不能長期活得體面的關鍵。
鎖隱藏的成本比你想象的要殘酷。它不光保護了那片內存,它更會默默地把并發操作串行化。一個持有鎖的慢操作,比如里面包了一層發送通知郵件、調一下外部接口、再寫幾條審計日志,就會把所有等著同一把鎖的 goroutine 擋在身后排長隊。表面上你只是 mutex 護住了一個字段的短暫改寫;架構上,這相當于你給一條本來可以并行通過的通道,硬塞了一個單車道收費站。
Hoare 在 1978 年給出的解法,就是把“通信”提升到和“共享內存”平級的位置,甚至把后者趕下王座。并發執行的不是需要互相窺探內存塊的東西,而是各自獨立、通過消息傳遞來協作的“進程”。Go 把這種“進程”縮成了 goroutine,把消息傳遞做成了 channel,然后告訴你:如果你不想半夜起來修死鎖,就別老想著讓一堆 goroutine 搶著摸同一片內存;讓數據在它們之間流動,誰處理后就把結果發到下一個站。
你沒法靠加鎖來獲得一個容易推理的并發系統。安全感和可維護性之間,隔著整整一套思維模型——而這篇比我們大多數人都老得多的論文,早早地把答案放在那里了。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.