活鎖是正在主動執行并發操作的程序,但這些操作無法向前移動程序的狀態。
你有沒有在走廊走向另一個人? 她移動到一邊讓你通過,但你也是這樣做的。 所以你轉移到另一邊,但她也是這樣做的。 想象這會永遠持續下去,這就是活鎖。
接下來的這個例子,我不建議試圖了解它的細節,直到你牢牢掌握sync包。 相反,我建議遵循代碼標注來理解高亮,然后將注意力轉移到包含示例核心的第二個代碼塊。
```
cadence := sync.NewCond(&sync.Mutex{})
go func() {
for range time.Tick(1 * time.Millisecond) {
cadence.Broadcast()
}
}()
takeStep := func() {
cadence.L.Lock()
cadence.Wait()
cadence.L.Unlock()
}
tryDir := func(dirName string, dir *int32, out *bytes.Buffer) bool { //1
fmt.Fprintf(out, " %v", dirName)
atomic.AddInt32(dir, 1) //2
takeStep() //3
if atomic.LoadInt32(dir) == 1 {
fmt.Fprint(out, ". Success!")
return true
}
takeStep()
atomic.AddInt32(dir, -1) //4
return false
}
var left, right int32
tryLeft := func(out *bytes.Buffer) bool { return tryDir("left", &left, out) }
tryRight := func(out *bytes.Buffer) bool { return tryDir("right", &right, out) }
```
1. tryDir 允許一個人嘗試向某個方向移動并返回,無論他們是否成功。 每個方向都表示為試圖朝這個方向移動的次數。
2. 首先,我們通過將該方向遞增1來朝著某個方向移動。 我們將在第3章詳細討論atomic包。現在,你只需要知道這個包的操作是原子操作。
3. 每個人必須以相同的速度或節奏移動。 takeStep模擬所有動作之間的恒定節奏。
4. 在這里,這個人意識到他們不能在這個方向上放棄。 我們通過將該方向遞減1來表示這一點。
```
walk := func(walking *sync.WaitGroup, name string) {
var out bytes.Buffer
defer func() { fmt.Println(out.String()) }()
defer walking.Done()
fmt.Fprintf(&out, "%v is trying to scoot:", name)
for i := 0; i < 5; i++ { //1
if tryLeft(&out) || tryRight(&out) { //2
return
}
}
fmt.Fprintf(&out, "\n%v tosses her hands up in exasperation!", name)
}
var peopleInHallway sync.WaitGroup //3
peopleInHallway.Add(2)
go walk(&peopleInHallway, "Alice")
go walk(&peopleInHallway, "Barbara")
peopleInHallway.Wait()
```
1. 我對嘗試次數進行了人為限制,以便該程序結束。 在一個有活鎖的程序中,可能沒有這種限制,這就是為什么它是一個現實工作中的問題。
2. 首先,這個人會試圖向左走,如果失敗了,會嘗試向右走。
3. 這個變量為程序提供了等待,直到兩個人都能夠相互通過或放棄。
程序會產生如下輸出:
```
Alice is trying to scoot: left right left right left right left right left right Alice tosses her hands up in exasperation!
Barbara is trying to scoot: left right left right left right left right left right
Barbara tosses her hands up in exasperation!
```
你可以看到Alice和Barbara在最終放棄之前持續交互。
這個例子演示了一個非常常見的活鎖寫入原因:兩個或多個并發進程試圖在沒有協調的情況下防止死鎖。 如果走廊里的人們一致認為只有一個人會移動,那么就不會有活鎖:一個人靜止不動,另一個人移動到另一邊,他們會繼續走路。
在我看來,活鎖比死鎖更難以發現,因為它看起來好像程序正在工作。 如果活鎖程序在你的機器上運行,并且你查看了CPU利用率以確定它是否在執行任何操作,那么你可能會認為它是。 根據活鎖的不同,它甚至可能會發出其他信號,使你認為它正在工作。 然而,一直以來,你的程序都扮演著走廊洗牌的永恒游戲。
活鎖是饑餓的問題的一個子集。 接下來我們會討論。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來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運行時
- 任務調度