2019.12读书笔记
《go语言并发之道》的读书笔记。
go语言并发之道
第一章
用了3个示例分别描述 死锁,活锁,饥饿。
凡是用到的锁多于2个,死锁都是要考虑的问题。
活锁在实际开发中还没有碰到过。
我对饥饿的理解,饥饿是锁的设计问题,不是程序员怎么用锁的问题,go的锁实现是有考虑饥饿状态的。我觉得书中饥饿的示例不恰当,greedy 协程的 count 为 20,polite 协程的 count 为 10,greedy 获取了 20 次锁,polite 获取了 10 * 3 = 30 次锁,怎么能说 polite 处于饥饿?
第二章
转让数据所有权用 channel 。
多个逻辑片段用 channel (select && case) 。
维护结构体内部状态用 mutex 。
性能要求高用 mutex 。
纯 csp 肯定是不现实的,我们是工程师,应该做一个实用主义者,实用简洁是 go 最吸引人的地方。
第三章
讲了 go 里面的各种并发模型,还讲了临时对象池 sync.Pool ,没有讲原子操作。
只要有了 3 种能力:1. 原子操作(cpu指令提供)2.协程阻塞(runtime提供)3.协程唤醒(runtime提供),我们就能实现各种各样的并发模型!
第四章
for-select循环,防止goroutine泄露(done channel),错误处理(wrap一层向外传递),context,pipeline 比较常见。
or-channel(用递归给多个channel加上或逻辑),tee-channel(将一个channel的输出复制到多个channel),桥接channel(chan chan int),队列排队(用一个缓冲channel解耦) 有待实践。
扇出的关键是stage中的任务可以轻易的分解,如果这个stage是排序那就没有这么简单了。
第五章
常见并发模式的简单使用。
复制请求,通过任务冗余提高响应速度,代价就是昂贵的资源。
速率限制,通过x/time/rate库(我猜实现方式是有个协程按给定的速率增加一个同步量)实现限速。
第六章
正在执行的线程从队列的尾部进栈出栈任务,其他线程从队列头部窃取续体,因为尾部的任务更有可能完成join,也是最有可能在cpu缓存中的。
测试协程调度顺序,得出了不同的结论,测试代码1:
// go v1.13
package main
import (
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
go func(a int) {
println(a)
}(i)
}
time.Sleep(10*time.Second)
}
输出:
0
1
2
... // 按顺序
8
9
新创建的协程加到队尾,从队头开始执行。
测试代码2:
// go v1.13
package main
import (
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
go func(a int) {
time.Sleep(3*time.Second) // 阻塞调用
println(a)
}(i)
}
time.Sleep(10*time.Second)
}
输出
9
0
1
... // 按顺序
7
8
被阻塞的协程加到队头,并将当前队头的协程踢到队尾,从队头开始执行。