饥饿是指并发进程无法获得执行工作所需的任何资源的情况。
当我们讨论活锁时,每个goroutine所缺乏的资源就是一个共享锁。 活锁需要与饥饿分开讨论,因为在活锁过程中,所有并发进程都是平等的,并且没有任何任务可以被完成。 更广泛地说,饥饿通常意味着有一个或多个贪婪的并发进程不公平地阻止一个或多个并发进程尽可能有效地完成工作,或者根本不可能完成工作。
下面这个例子展示了一个贪婪的goroutine和一个知足的goroutine:
```
var wg sync.WaitGroup
var sharedLock sync.Mutex
const runtime = 1*time.Second
greedyWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime; {
sharedLock.Lock()
time.Sleep(3*time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Greedy worker was able to execute %v work loops\n", count)
}
politeWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime; {
sharedLock.Lock()
time.Sleep(1*time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1*time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1*time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Polite worker was able to execute %v work loops.\n", count)
}
wg.Add(2)
go greedyWorker()
go politeWorker()
wg.Wait()
```
这个代码段会输出:
```
Polite worker was able to execute 289777 work loops. Greedy worker was able to execute 471287 work loops
```
greedy 贪婪地持有整个工作循环的共享锁,而polite 试图只在需要时才锁定。 二者都进行了相同数量的模拟工作(休眠时间为三纳秒),但正如你在相同的时间内看到的那样,greedy 几乎完成了两倍的工作量!
但在这里我们要清楚的了解到,greedy不必要的扩大了对共享锁的控制,并且(通过饥饿)阻碍了polite有效的执行。
我们在例子中使用计数的方式识别饥饿,在记录和抽样度量指标时这是一个很不错的方法。检测和解决饥饿的方法之一就是就是记录程序完成的时间,然后确定你的程序执行速度是否与预期的一样高。
> 值得一提的是,前面的代码示例也可以作为进行同步内存访问的性能分支示例。 因为同步访问内存的代价很高,所以扩大我们的锁定范围可能会产生额外的代价。 另一方面,正如我们所看到的那样,我们冒着令其他并发进程挨饿的风险。
> 如果你利用内存访问同步,你必须在性能粗粒度同步和公平性细粒度同步之间找到平衡点。 当开始调试应用程序时,我强烈建议你将内存访问同步仅限于程序的关键部分; 如果同步成为性能问题,则可以扩大范围。 除此之外,其他的解决方式可能会更难以操作。
因此,饥饿可能会导致程序无效或不正确。 前面的例子表明了执行效率是如何被降低的,如果你有一个非常贪婪的并发进程,以至于完全阻止另一个并发进程完成工作,那么你的问题就大了。
我们还需要考虑来自程序之外导致的饥饿问题。请记住,饥饿还可以产生于于CPU,内存,文件句柄和数据库连接:任何必须共享的资源都是饥饿的候选对象。
* * * * *
学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
感谢beego群(258969317)的"赤脚大仙"提出代码部分格式化的修改意见,代码格式缺失已作调整。
- 前序
- 谁适合读这本书
- 章节导读
- 在线资源
- 第一章 并发编程介绍
- 摩尔定律,可伸缩网络和我们所处的困境
- 为什么并发编程如此困难
- 数据竞争
- 原子性
- 内存访问同步
- 死锁,活锁和锁的饥饿问题
- 死锁
- 活锁
- 饥饿
- 并发安全性
- 优雅的面对复杂性
- 第二章 代码建模:序列化交互处理
- 并发与并行
- 什么是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运行时
- 任务调度