在使用并發代碼時,安全操作有幾種不同的選項。 我們已經使用并了解了其中兩個:
* 用于共享內存的同步原語(例如sync.Mutex)
* 通過通信同步(例如channel)
此外,有多個其他選項在多個并發進程中隱式安全:
* 不可變數據
* 受限制條件保護的數據
從某種意義上講,不可變數據是最理想的,因為它隱式地是并行安全的。每個并發進程可以在同一條的數據上運行,但不能修改它。如果要創建新數據,則必須創建所需修改數據的副本。 這不僅可以減輕開發人員認知負擔,還可以讓程序執行的更快(在某些情況下)。在Go中,可以通過使用值的副本而非該值的指針來實現此目的。 有些語言支持使用明確不變的值的指針; 然而,Go不在其中。
不可變數據的使用依賴于約定——在我看來,堅持約定很難在任何規模的項目上進行協調,除非你有工具在每次有人提交代碼時對代碼進行靜態分析。這里就有一個例子:
```
data := make([]int, 4)
loopData := func(handleData chan<- int) {
defer close(handleData)
for i := range data {
handleData <- data[i]
}
}
handleData := make(chan int)
go loopData(handleData)
for num := range handleData {
fmt.Println(num)
}
```
我們可以看到,loopData函數和對handleData通道的循環都使用了整數切片data,但只有loopData對其進行了直接訪問。
但想想看,隨著代碼被其他的開發人員觸及和修改,明顯的,不明顯的問題都有可能會被加入其中,并最終產生嚴重的錯誤(因為我們沒有對data切片做顯示的訪問和操作約束)。正如我所提到的,一個靜態分析工具可能會發現這類問題,但如此靈活的靜態分析并不是很多團隊能夠實現的。 這就是為什么我更喜歡詞匯約束,使用編譯器來執行對變量的操作進行約束是非常好的。
詞法約束涉及使用詞法作用域僅公開用于多個并發進程的正確數據和并發原語。 這使得做錯事情變得不可能。 實際上,我們在第3章已經談到了這個話題。回想一下通道部分,它討論的只是將通道的讀或寫操作暴露給需要它們的并發進程。 我們再來看看這個例子:
```
chanOwner := func() <-chan int {
results := make(chan int, 5) //1
go func() {
defer close(results)
for i := 0; i <= 5; i++ {
results <- i
}
}()
return results
}
consumer := func(results <-chan int) { //3
for result := range results {
fmt.Printf("Received: %d\n", result)
}
fmt.Println("Done receiving!")
}
results := chanOwner() //2
consumer(results)
```
1. 這里我們在chanOwner函數的詞法范圍內實例化通道。這將導致通道的寫入操作范圍被限制在它下面定義的閉包中。 換句話說,它限制了這個通道的寫入使用范圍,以防止其他goroutine寫入它。
2. 在這里,我們接受到一個只讀通道,我們將它傳遞給消費者,消費者只能從中讀取信息。
3. 這里我們收到一個int通道的只讀副本。通過聲明該函數的唯一用法是讀取訪問,我們將通道用法限制為只讀。
這樣的設計方式就可以把通道的讀取寫入限制在一定的范圍內。這個例子可能不是非常的有趣,因為通道是并發安全的。我們來看一個對非并發安全的數據結構約束的示例,它是一個bytes.Buffer實例:
```
printData := func(wg *sync.WaitGroup, data []byte) {
defer wg.Done()
var buff bytes.Buffer
for _, b := range data {
fmt.Fprintf(&buff, "%c", b)
}
fmt.Println(buff.String())
}
var wg sync.WaitGroup
wg.Add(2)
data := []byte("golang")
go printData(&wg, data[:3]) // 1
go printData(&wg, data[3:]) // 2
wg.Wait()
```
1. 這里我們傳入包含前三個字節的data切片。
2. 這里我們傳入包含剩余三個字節的data切片。
在這個例子中,你可以看到,我們不需要通過通信同步內存訪問或共享數據。
那么這樣做有什么意義呢? 如果我們有同步功能,為什么要給予約束? 答案是提高了性能并降低了開發人員的認知負擔。同步帶來了成本,如果你可以避免它,你就不必支付同步它們的成本。 你也可以通過同步回避所有可能的問題。利用詞法約束的并發代碼通常更易于理解。
話雖如此,建立約束可能很困難,所以有時我們必須回到使用并發原語的開發思路上去。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來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運行時
- 任務調度