昨天参加了 Gopher China 2019 大会, 分享了《Go 并发编程实践》的主题, 在这一篇博客中总结一下.
从春节前开始我就有意识的收集和整理 Go 并发编程的相关知识, 以前也有相关的博文进行过分享, 本来是想以电子书的方式进行分享, 正好 Asta Xie 邀请在 Gopher China 2019 上做一个分享, 就毫不犹豫的答应了.
谈起 Go 并发编程的分享, 每年各地的 Go 相关的大会都会有, 基本上都会针对某一个专题进行介绍, 或者针对 Go 并发编程的哲学进行分享. 所以我有两个选择, 一个是结合业务讲一些在企业开发中的实践, 另外一个是对 Go 并发编程做一个全面的总结.
我选择了后一个, 也是最难的一个. 一是内容非常多, 而是不结合业务, 只讨论代码和实现, 多少会比较枯燥, 尤其是针对主要关注于业务同学, 但是对于热心于 Go 编程的语言的开发者来说, 能有人对 Go 并发进行一个深入而全面的总结, 对于自己回顾和整理 Go 并发编程的技能, 多多少少会有所帮助的, 所以我愿意进行一次尝试.
最初开始整理了很多的知识点, 内容太多了, 我忍痛删去了 Go 并发编程设计模式一节, 第一版 120 多页的 ppt 发给 Asta 之后, Asta 感觉还是太多, 又忍痛删去了分布式并发原语这一节, 最后整理了 81 页的 PPT. 我是实在不想删减了, 再删减, 就变成了《Go 标准 sync 库在实践中的几个坑》, 就偏离了我最初的分享的目的: 全面总结 Go 并发编程知识, 而退化成第一个选择: 找一个直接点结合业务进行分享.
80 页的 PPT 想在 45 分钟内顺利的讲完, 并且不讲业务只讲 Go 语言的特性, 非常的困难, 我预想到会拖堂, 实际也做到了, 所以后面的介绍比较的匆忙了, 希望这篇文章能对会后大家的理解能够更顺畅一些, 也希望对未能参加会议的朋友能有所帮助.
我也会听取读者和 Gopher 的意见, 来决定是否讲删掉了章节补上, 以 PPT 或者电子书的方式提供出来.
内容划分
Go 并发编程的知识点非常多, 最基本的 goroutine 因为非常简单, 就不介绍了. 大致把知识点划分成下面的五部分.
基本同步原语
Mutex
最常用的 sync 包下的同步原语之一.
自 2008 年开始, 经过了几次大的修改, 加入了公平性和性能综合的考量, 饥饿的处理, 今年又进行了内敛的优化, 对功能和性能都有了很好的提升.
内部结构使用 state 标记是否加锁, 唤醒, 饥饿等状态, 使用高位来记录等待者的数量.
虽然 state 是 unexported 的, 但是你可以通过 unsafe 包 hack 方式读取这些状态.
Unlock 未加锁或者已解锁的 Mutex 会 panic
Mutex 不会比较当前请求的 goroutine 是否已经持有这个锁, 所以可以一个 goorutine Lock, 另一个 goroutine Unlock, 但是慎用, 避免死锁
非重入锁 , Java 程序员转 Go 容易犯的错, 会导致死锁. 如果想重入, 使用扩展的同步原语.
RWMutex
读写锁对于读进行了优化, 适合写少读多的状态, 对并发的读很适合.
如果有 goroutine 持有了 RWMutex, 那么只可能被一组 reader 持有, 或者被一个 writer 持有.
如果已经有一组 reader 持有了读写锁, 这个时候如果 writer 调用 Lock, 它会被阻塞. 接着如果有 reader 调用 RLock, 等前面那组 readerUnlock 后, writer 优先获取锁
不要在递归调用读锁. 因为上一条的规定, 递归调用锁容易导致死锁
可以讲读锁返回成一个 Locker 的接口
Cond
类似 Monitor, 提供了通知的机制, 可以 Broadcast 通知所有 Wait 的 goroutine, 也可以 Signal 通知某一个 Wait 的 goroutine.
Cond 初始化的时候要传入一个 Locker 接口的实现, 这个 Locker 可以用来保护条件变量.
Broadcast 和 Signal 不需要加锁调用, 但是调用 Wait 的时候需要加锁.
Wait 执行中有解锁重加锁的过程, 在这个期间对临界区是没有保护的.
一定要使用 for 循环来检查条件是否满足, 因为随时都可以触发通知.
Waitgroup
也是最常用的 sync 包下的同步原语之一.
内部通过一个计数器来记录 waiter.
在 Wait 之前可以设置这个计数器的数量. 等这个计数器为 0 的时候, 所有等待的 goroutine 都都会解除等待, 继续执行.
Add 方法可以增加技术, 也可以传入负值减少技术, 但是如果计数器小于 0 的情况下会 panic.
Done 方法是利用 - 1 实现的, 因此 Done 的次数如果多于计数器, 会 panic.
Wait 调用多次没问题, 只要计数器为 0, 它就不会阻塞.
并发 Add 和 Wait 会 panic.
前一个 Wait 还没有完成就 Add 也会 panic.
所以 Waitgroup 是可以 重用 的, 但是一定等前一个 Wait 完成后再重用.
Once
用来初始化一次, 比如实现单例, 单元测试时环境的准备.
不要在传给 Do 的函数中调用这个 Once, 否则会死锁.
即使传入的这个函数会 panic,Once 也认为它已经初始化了.
Go 单例的实现:
常量
package 变量 (eager)
init 函数 (eager)
GetInstance() (lazy)
通过 sync.Once 或者类似实现
A XXX must not be copied after first use.
看上面的同步原语的 godoc, 都有这么一句话. 对象使用后就不能被复制了.
这是因为使用后这些对象都是有状态的, 复制过去也会把状态复制过去, 比如已加锁的状态, 这不是我们期望的.
可以通过 go vet 工具检查.
如果你定义的 struct 也想有这个功能, 可以使用 noCopy 这种经济的方式, 定义 Locker 接口, 让 vet 工具也能检查.
简单的复制是容易看出来的, 很多隐藏的复制检查可以通过工具.
Pool
临时对象池
可能在任何时候任意的对象都可能被移除
可以安全地并发访问
装箱 / 拆箱
tcp, 数据库连接池的话不要使用它, 使用专门的池.
标准库中有的池的实现使用它, 有的需要永久持有的对象不使用它, 而是使用链表, 比如 rpc.
用它做 buffer 池要注意, 避免内存泄漏. Pool 的官方例子和标准库 fmt,JSON 中都有这个坑. 标准库中已经修复了.
Map
使用空间换时间的方式, 提供下面两个场景下的性能:
设置一次, 多次读, 比如 cache
多个 goroutine 并发的读, 写, 更新不同的 key
有以下的考量:
装箱 / 拆箱
Range 进行遍历, 可能会加锁
没有 Len 方法, 并且也不会添加
扩展同步原语
对基本同步原语的补充, 适用于额外的场景, 由 Go 扩展包 (试验包) 和第三方提供.
ReentrantLock
标准库 sync 下的 Mutex 是不能重入的, 如果想实现重入的话, 可以利用:
goid: 用来标记谁持有了当前锁, 重入了几次
全局 id: 或者你自己维护一个全局 id, 但是实现的结构不再满足 Locker 接口
可重入锁又交递归锁, 但是叫可重入锁更准确.
Semaphore
Dijkstra 提出并发访问通用资源的并发原语, 使用 PV 原语提供对临界区的保护.
二进制 (取值 0,1) 的 semaphore 提供了锁的功能.
计数器 semaphore 提供了对一组资源的保护.
包 golang.org/x/sync/semaphore .
标准库内部的 semaphore 提供了休眠 / 唤醒的功能, 用来实现基本同步原语的阻塞.
SingleFlight
并发的访问同一组资源的时候, 只允许一个请求进行, 这个请求把结果告诉其它等待者, 避免雪崩的现象.
比如 cache 失效的时候, 只允许一个 goroutine 从数据库中捞数据回种, 避免雪崩对数据库的影响.
扩展库中提供.
ErrGroup
应用于 half sync/half async 的场景(这个设计模式以后有机会再介绍).
有一组异步的任务需要处理, 利用这个原语可以等待所有的异步任务完成, 并获取第一个错误.
如果想得到所有的错误, 利用额外的 map 变量进行记录.
使用 Context 可以实现遇到第一个错误就返回.
扩展包中提供.
bilibili 扩展了这个原语, 提供了限定并发数量的功能.
SpinLock
自旋锁
有时候很高效, 因为当前 CPU 中运行的 goroutine 更有机会获取到锁
不公平
需要处理器忙等待
应用于竞争不是很激烈的状态
fslock
文件锁, 可以控制多个进程之间的同步.
concurrent-map
类似 Java 中的 ConcurrentMap 的设计思想, 将 key 划分成一定数量的 shard, 每个 shard 一个锁, 减少锁的竞争.
相对于 sync.Map, 可以应用写 / 删除 / 添加更频繁的场景.
原子操作
保证操作是原子的.
操作的数据
- int32
- int64
- uint32
- uint64
- uintptr
- unsafe.Pointer
操作方法
- AddXXX (整数类型)
- CompareAndSwapXXX:cas
LoadXXX: 读取
StoreXXX: 存储
SwapXXX: 交换
Subtract
有符号的类型, 可以使用 Add 负数
无符号的类型, 可以使用 AddUint32(&x, ^uint32(c-1)),AddUint64(&x, ^uint64(c-1))
无符号类型减一, AddUint32(&x, ^uint32(0)), AddUint64(&x, ^uint64(0))
Value
一个通用的对象, 可以很方便的对 struct 等类型进行原子存储和加载.
由于不同的架构下对原子操作的支持是不一样的, 有些架构师是不支持的.
Channel
channel 容易再犯错, 甚至于比使用传统 sync 包下的同步原语的错误率还要高, 牢记异常的情况:
close 已经 close 的 channel 也会 panic.
利用 channel 实现锁, 因为利用 Go 的内存模型可以保障, 但是正常情况 channel 和 mutex 有不同的应用场景.
Channel
传递数据的 owner
分发任务
交流异步结果
任务编排
Mutex
cache
状态
临界区
Channel 的一些应用模式可以参考另一篇文章: Go Channel 应用模式
Go 内存模型
内存模型描述了线程 (goroutine) 通过内存的交互, 以及对数据的共享使用.
Java 语言是第一个详细描述其内存模型的流行的编程语言.
它并不是描述内存是如何分配的, 而是定义了:
对同一个变量, 如何保证在一个 goroutine 对此变量读的时候, 能观察到其它 goroutine 对此变量的写.
描述这种顺序关系的术语叫做 happen before .
单个 goroutine 内
执行顺序和代码编写的顺序是一致的(有 reorder, 也不影响理解, 可以按照编写顺序进行分析)
包级别的 init 函数
在单个 goroutine 中执行
最底层引入的包的 init 先执行. 之后再是 main 函数.
提供问题: 同一个包下可以定义多个 init 函数吗?
go 语句
goroutine 的创建 happens before 所有此 goroutine 中的操作
goroutine 的销毁 happens after 所有此 goroutine 中的操作
channel
第 n 个 send 一定 happen before 第 n 个 receive 完成, 不管是 buffered channel 还是 unbuffered channel
对于 capacity 为 m 的 channel, 第 n 个 receive 一定 happen before 第 (n+m) send 完成
m=0 unbuffered. 第 n 个 receive 一定 happen before 第 n 个 send 完成
channel 的 close 一定 happen before receive 端得到通知, 得到通知意味着 receive 收到一个因为 channel close 而收到的零值
注意 send/send completes,receive/receive completes 的区别
Mutex/RWMutex
对于 Mutex/RWMutx m, 第 n 个成功的 m.Unlock 一定 happen before 第 n+1 m.Lock 方法调用的返回
对于 RWMutex rw, 如果它的第 n 个 rw.Lock 已返回, 那么它的第 n 个成功的 rw.Unlock 的方法调用一定 happen before 任何一个 rw.RLock 方法调用的返回(它们 happen after 第 n 个 rw.Lock 方法调用返回)
对于 RWMutex rw, 如果它的第 n 个 rw.RLock 已返回, 接着第 m (m < n)个 rm.RUnlock 方法调用一定 happen before 任意的 rw.Lock(它们 happen after 第 n 个 rw.RLock 方法调用返回之后)
Waitgroup
对于 Waitgroup b, 对于其计数器不是 0 的时候, 假如此时刻之后有一组 wg.Add(n), 并且我们确信只有最后一组方法调用使其计数器最后复原为 0, 那么这组 wg.Add 方法调用一定 happen before 这一时刻之后发生的 wg.Wait
wg.Done()也是 wg.Add(-1)
Once
once.Do 方法的执行一定 happen before 任何一个 once.Do 方法的返回
Atomic
没有官方的保证
建议是不要依赖 atomic 保证内存的顺序
#5045 历史悠久的讨论, 还没 close
来源: http://www.tuicool.com/articles/nI7fqqB