活锁是正在主动执行并发操作的程序,但这些操作无法向前移动程序的状态。
你有没有在走廊走向另一个人? 她移动到一边让你通过,但你也是这样做的。 所以你转移到另一边,但她也是这样做的。 想象这会永远持续下去,这就是活锁。
接下来的这个例子,我不建议试图了解它的细节,直到你牢牢掌握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运行时
- 任务调度