《go语言并发之道》读书笔记

416 阅读2分钟

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

被阻塞的协程加到队头,并将当前队头的协程踢到队尾,从队头开始执行。