CSP過去和現在都是Go設計的重要組成部分; 然而,Go還支持通過內存訪問同步和遵循該技術的基元來編寫并發代碼的更傳統手段。 同步和其他軟件包中的結構和方法允許你執行鎖定,創建資源池,搶占goroutine等。
這些能力對你來說將非常有用,因為它可以讓你對選擇編寫哪種類型的并發代碼來解決問題擁有更大的自由度,但它也可能有點混亂。 該語言的新手經常會得到這樣的印象:并發CSP風格被認為是在Go中編寫并發代碼的唯一方法。 例如,在同步軟件包的文檔中,它說:
>sync包提供基本的同步原語,如互斥鎖。 除了Once和WaitGroup類型之外,大部分類型都是供底層庫例程使用的。 通過通道和通信可以更好地完成更高級別的信息交互。
在官方FAQ文檔中提到:
>關于互斥鎖,sync包實現它們,但我們希望Go編程風格將鼓勵人們嘗試更高級的技術。 特別是,考慮構建你的程序,這樣一次只有一個goroutine負責某個特定的數據。
>不要通過共享內存進行通信。 相反,通過通信共享內存。
還有很多文章,講座和采訪,Go核心庫大多都支持CSP風格,比如sync.Mutex。
因此,Go團隊為什么選擇公開內存訪問同步原語會感到困惑是完全可以理解的。 更令人困惑的是,你會看到通常會出現的同步原語,看到人們抱怨過度使用通道,并且還聽到一些Go團隊成員表示可以使用它們。 這是來自Go Wiki的關于此事的引文:
> Go的格言之一是“通過溝通共享內存,不要通過共享內存進行通信”。
> 也就是說,Go確實在sync包中提供了傳統的鎖定機制。 大多數鎖定問題都可以使用通道或傳統鎖來解決。
那么你應該使用哪個?
>使用最具表現力和/或最簡單的。
這是很好的建議,這是你在使用Go時經常看到的指導方針,但它有點含糊。 我們如何理解什么更具表現力和/或更簡單? 我們可以使用什么標準? 幸運的是,可以使用一些指導來幫助我們做正確的事情。 正如我們將會看到的那樣,主要區分的方式來自于試圖管理并發性的地方:從內部到緊密的范圍,或者在整個系統中。 下圖列舉了這些指標:
:-: 
讓我們一步接一步的看這幅圖:
#### 你想轉移數據的所有權嗎?
如果你有一些代碼能夠產生結果,并希望與另一部分代碼共享這個結果,那么你真正在做的是轉移那些數據的所有權。 如果你熟悉不支持垃圾回收的語言的內存所有權概念,那么也可以稱之為:數據所有者,使并發程序安全的一種方法是確保只有一個并發上下文擁有數據的所有權 。 通道可以幫助我們來傳達這一概念。
這樣做的一大好處是你可以創建緩沖通道來實現資源廉價的內存隊列,從而將你的生產者與消費者分離。 另一個是通過使用通道,你可以隱式地將你的并發代碼與其他并發代碼組合在一起。
#### 你是否試圖保護結構的內部狀態?
這是內存訪問同步原語的一個很好的選擇,也是一個非常強大的指示器,你不應該使用通道。 通過使用內存訪問同步原語,你可以隱藏從呼叫者鎖定關鍵部分的實現細節,但不會給調用者帶來復雜性。 這是一個線程安全類型的小例子:
```
type Counter struct {
mu sync.Mutex value int
}
func(c *Counter) Increment()
{
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
```
如果你回想一下原子性的概念,我們可以說在這里所做的是定義了Counter類型的原子性范圍。 調用增量可以被認為是原子的。
記住這里的關鍵詞是"內部的"。 如果你發現自己超出鎖定范圍,這應該會引起重視。 盡量將鎖限制在一個小的范圍內。
#### 你是否想要協調多個邏輯?
請記住,通道本質上比內存訪問同步基元更具可組合性。 將鎖分散在各個結構中聽起來像是一場噩夢。
如果你因Go的選擇語句而使用通道,且能夠充當隊列安全地傳遞,你會發現控制軟件中出現的緊急復雜性要容易得多。 如果你發現自己在努力了解并發代碼的工作原理,為什么會發生死鎖或競爭,并且你正在使用基元,這可能是你需要切換到通道的一個很好的信號。
#### 這是一個性能的關鍵部分嗎?
這絕對不意味著,“我希望我的程序是高性能的,因此我只會使用互斥鎖。”相反,如果你有一部分程序是已經分析過的,并且事實證明它是一個主要的瓶頸,當你發現這里比程序的其余部分慢一些,使用內存訪問同步原語可以幫助這個關鍵部分在負載下執行。由于通道使用內存訪問同步來操作,因此它們只能更慢。
希望這可以清楚地說明是否利用CSP風格的并發或內存訪問同步。 還有其他一些模式和做法在使用操作系統線程作為抽象并發的方式的語言中很有用。 例如,像線程池這樣的東西經常出現。 因為這些抽象的大部分是針對OS線程的優點和缺點的,所以使用Go時的一個很好的經驗法則是放棄這些模式。
這并不是說它們根本沒有用處,而是Go中的用例受到了更多限制。 堅持用goroutines為你的問題建模,用它們來代表你的工作流程的并發部分,并且不要害怕在啟動它們時變得自由。 你很可能需要重新構建你的程序,而不是關注你的硬件可以支持多少個goroutines的上限。
Go的并發理念可以這樣概括:為了簡單起見,在可能的情況下使用通道,并且像免費資源一樣處理goroutine(而不需要過多過早的考慮資源占用情況)。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度