假設我們有一個數據競爭:兩個并發進程試圖訪問同一個內存區域,并且它們訪問內存的方式不是原子的。 我們對之前的例子進行一些修改:
```
var data int
go func() { data++}()
if data == 0 {
fmt.Println("the value is 0.")
} else {
fmt.Printf("the value is %v.\n", data)
}
```
我們在這里添加了一個else子句,以便不管數據的值如何,總會得到一些輸出。 請記住,正如它所寫的那樣,存在數據競爭,并且程序的輸出將完全不確定。
程序中有一些操作需要獨占訪問共享資源。在這個例子中,我們找到三處:
* goroutine正在增加數據變量。
* if語句,它檢查數據的值是否為0。
* fmt.Printf語句,用于檢索輸出數據的值。
有很多方法可以保護這些訪問,Go有很好的方式來處理這個問題,解決這個問題的方法之一是讓這些操作同步訪問內存。 讓我們看看該怎樣做到這一點。
下面的代碼不是Go的慣用法(我不建議你像這樣解決數據競爭問題),但它很簡單地演示了內存訪問同步。如果這個例子中的任何類型,函數或方法對你來說都很陌生,那沒問題。跟蹤注釋,關注同步訪問內存的概念就行。
```
var memoryAccess sync.Mutex //1
var value int
go func() {
memoryAccess.Lock() //2
value++
memoryAccess.Unlock() //3
}()
memoryAccess.Lock() //4
if value == 0 {
fmt.Printf("the value is %v.\n", value)
} else {
fmt.Printf("the value is %v.\n", value)
}
memoryAccess.Unlock() //5
```
1. 這里我們添加一個變量,它允許我們的代碼同步對數據變量內存的訪問。第三章的sync包會介紹sync.Mutex類型的細節。
2. 在這里我們聲明,除非解鎖,否則我們的goroutine應該獨占訪問此內存。
3. 在這里,我們聲明這個對該內存的訪問已經完成了。
4. 在這里,我們再次聲明接下來的條件語句應該獨占訪問數據變量的內存。
5. 在這里,我們聲明對內存的訪問已經完成。
在這個例子中,我們為開發者制定了一個約定。任何時候開發人員都想訪問data變量的內存,必須首先調用Lock,當完成訪問操作時,必須調用Unlock。這兩個語句之間的代碼可以假定它擁有對數據的獨占訪問權; 我們已經成功地同步了對內存的訪問。注意,如果開發者不遵循這個約定,我們就沒有保證獨占訪問的權利!
你可能已經注意到,雖然我們已經解決了數據競爭,但我們并沒有真正解決競爭條件!這個程序的操作順序仍然不確定。 我們剛剛只是縮小了非確定性的范圍。在這個例子中,仍然不確定goroutine是否會先執行,或者我們的if和else塊是否都會執行。 稍后,我們將探索正確解決這類問題的工具。
從表面上看,這似乎很簡單:如果你發現你有這樣的需求,添加點來同步訪問內存! 很簡單,對吧?
但是!
確實,你可以通過同步訪問內存來解決一些問題,但正如我們剛剛看到的,它不會自動解決數據競爭或邏輯正確性問題。 此外,它還可能導致維護和性能問題。
請注意,之前我們提到已經創建了一個聲明需要對某些內存進行獨占訪問的約定。約定本身是沒問題的,但實際開發中我們總是會丟三落四。更別說開發組里總會有這樣或那樣不遵守約定的人。值得慶幸的是,隨后我們還將介紹一些更有效的方法。
以這種方式同步對內存的訪問會導致性能下降。每次我們執行其中一項操作時,程序會暫停一段時間。 這帶來了兩個問題:
* 加鎖的程序部分是否重復進入和退出?
* 加鎖的程序對內存占用到底有多大?
要說清這兩個問題簡直是門藝術。
同步對內存的訪問也與其他并發建模存在關聯,我們將在下一節討論這些問題。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來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運行時
- 任務調度