Go 并发编程一年回顾 (2021)

鸟窝

大道至简 Simplicity is the ultimate form of sophistication

首页 归档 github

网站群

Go 汇编示例 Go Web 开发示例 Go 数据库开发教程


RPCX 官网 RPC 开发指南

Scala 集合技术手册 关于

2021 年 11 月 09 日

Go

by smallnest

Go 并发编程一年回顾 (2021)

去年的时候我写了一篇Go 并发编程一年回顾, 如今 2021 年也快结束了,Go 1.18 的特性已经冻结,美国页很快进入了假期模式,趁这个节点,我们回顾一下近一年 Go 并发编程的进展。

TryLock 终于要发布

很久以来 (可以追溯到 2013 年#6123), 就有人提议给 Mutex 增加 TryLock 的方法,被大佬们无情的拒绝了,断断续续,断断续续的一直有人提议需要这个方法,如今到了 2021 年,Go team 大佬们终于松口了,增加了相应的方法 (#45435)。

一句话来说,Mutex 增加了 TryLock, 尝试获取锁, RWMutex 增加了 TryLock 和 TryRLock 方法,尝试获取写锁和读锁。它们都返回 bool 类型。如果返回 true, 代表已经获取到了相应的锁,如果返回 false, 则表示没有获取到相应的锁。

本质上,要实现这些方法并不麻烦,接下来我们看看相应的实现 (去除了 race 代码)。

首先是 Mutex.TryLock:

  1. func (m *Mutex) TryLock() bool {
  2. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  3. return true
  4. }
  5. return false
  6. }

也就是利用 aromic.CAS 操作 state 字段,如果当前没有被锁或者没有等待锁的情况,就可以成功获取到锁。不会尝试 spin 和与等待者竞争。

不要吐槽上面的代码风格,可能你觉得不应该写成下面的方式吗?原因在于我删除了 race 代码,那些代码块中包含 race 代码,所以不能像下面一样简写:

  1. func (m *Mutex) TryLock() bool {
  2. return atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)
  3. }

读写锁有些麻烦,因为它有读锁和写锁两种情况。

首先看 RWMutex.TryLock(去除了 race 代码):

  1. func (rw *RWMutex) TryLock() bool {
  2. if !rw.w.TryLock() {
  3. return false
  4. }
  5. if !atomic.CompareAndSwapInt32(&rw.readerCount, 0, -rwmutexMaxReaders) {
  6. rw.w.Unlock()
  7. return false
  8. }
  9. return true
  10. }

首先底层的 Mutex.TryLock, 尝试获取 w 字段的锁, 如果成功,需要检查当前的 Reader, 如果没有 reader, 则成功, 如果此时不幸还有 reader 没有释放读锁,那么尝试 Lock 也是不成功的, 返回 false。注意返回之前一定要把 rw.w 的锁释放掉。

接下来看 RWMutex.TryRLock(去除了 race 代码):

  1. func (rw *RWMutex) TryRLock() bool {
  2. for {
  3. c := atomic.LoadInt32(&rw.readerCount)
  4. if c < 0 {
  5. return false
  6. }
  7. if atomic.CompareAndSwapInt32(&rw.readerCount, c, c+1) {
  8. return true
  9. }
  10. }
  11. }

这段代码首先检查 readerCount, 如果为负值,说明有 writer,此时直接返回 false。

如果没有 writer, 则使用 atomic.CAS 把 reader 加 1, 如果成功,返回。如果不成功,那么此时可能有其它 reader 加入,或者也可能有 writer 加入,因为不能判断是 reader 还是 writer 加入,那么就用一个 for 循环再重试。

如果是 writer 加入,那么下一次循环 c 可能就是负数,直接返回 false, 如果刚才是有 reader 加入,那么它再尝试加 1 就好了。

以上就是新增的代码,不是特别复杂。Go team 不情愿的把这几个方法加上了, 同时有很贴心的提示 (恐吓):

Note that while correct uses of TryLock do exist, they are rare,
and use of TryLock is often a sign of a deeper problem
in a particular use of mutexes.

WaitGroup 的字段变化

先前,WaitGroup 类型使用[3]uint32作为state1字段的类型,在 64 位和 32 位编译器情况下,这个字段的 byte 的意义是不同的,主要是为了对齐。虽然使用一个字段很 “睿智”, 但是阅读起来却很费劲,现在,Go team 把它改成了两个字段,根据对齐规则,64 位编译器会对齐相应字段,讲真的,我们不差那 4 个字节。

  1. type WaitGroup struct {
  2. noCopy noCopy
  3. // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
  4. // 64-bit atomic operations require 64-bit alignment, but 32-bit
  5. // compilers only guarantee that 64-bit fields are 32-bit aligned.
  6. // For this reason on 32 bit architectures we need to check in state()
  7. // if state1 is aligned or not, and dynamically "swap" the field order if
  8. // needed.
  9. state1 uint64
  10. state2 uint32
  11. }
  12. // state returns pointers to the state and sema fields stored within wg.state*.
  13. func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
  14. if unsafe.Alignof(wg.state1) == 8 || uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
  15. // state1 is 64-bit aligned: nothing to do.
  16. return &wg.state1, &wg.state2
  17. } else {
  18. // state1 is 32-bit aligned but not 64-bit aligned: this means that
  19. // (&state1)+4 is 64-bit aligned.
  20. state := (*[3]uint32)(unsafe.Pointer(&wg.state1))
  21. return (*uint64)(unsafe.Pointer(&state[1])), &state[0]
  22. }
  23. }

64 位对齐情况下 state1 和 state2 意义很明确,如果不是 64 位对齐,还得巧妙的转换一下。

Pool 中使用 fastrandn 替换 fastrand

Go 运行时中提供了fastrandn方法,要比fastrand() % n快很多,相关的文章可以看下面中的注释中的地址。

  1. //go:nosplit
  2. func fastrand() uint32 {
  3. mp := getg().m
  4. // Implement wyrand: https://github.com/wangyi-fudan/wyhash
  5. if goarch.IsAmd64|goarch.IsArm64|goarch.IsPpc64|
  6. goarch.IsPpc64le|goarch.IsMips64|goarch.IsMips64le|
  7. goarch.IsS390x|goarch.IsRiscv64 == 1 {
  8. mp.fastrand += 0xa0761d6478bd642f
  9. hi, lo := math.Mul64(mp.fastrand, mp.fastrand^0xe7037ed1a0b428db)
  10. return uint32(hi ^ lo)
  11. }
  12. // Implement xorshift64+
  13. t := (*[2]uint32)(unsafe.Pointer(&mp.fastrand))
  14. s1, s0 := t[0], t[1]
  15. s1 ^= s1 << 17
  16. s1 = s1 ^ s0 ^ s1>>7 ^ s0>>16
  17. t[0], t[1] = s0, s1
  18. return s0 + s1
  19. }
  20. //go:nosplit
  21. func fastrandn(n uint32) uint32 {
  22. // This is similar to fastrand() % n, but faster.
  23. // See https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
  24. return uint32(uint64(fastrand()) * uint64(n) >> 32)
  25. }

所以 sync.Pool 中使用fastrandn做了一点点修改,用来提高性能。好卷啊,这一点点性能都来压榨, 关键,这还是开启 race 才会执行的代码。

sync.Value 增加了 Swap 和 CompareAndSwap 两个便利方法

如果使用 sync.Value, 这两个方法的逻辑经常会用到,现在这两个方法已经添加到标准库中了。

  1. func (v *Value) Swap(new interface{}) (old interface{})
  2. func (v *Value) CompareAndSwap(old, new interface{}) (swapped bool)

Go 1.18 中虽然实现了泛型,但是一些库的修改有可能在将来的版本中实现了。在泛型推出来之后,atomic 对类型的支持会有大大的加强,所以将来 Value 这个类型有可能退出历史舞台,很少被使用了。(参考 Russ Cox 的文章Updating the Go Memory Model)

整体来说,Go 的并发相关的库比较稳定,并没有大的变化。

[Older

Go 泛型系列:再简化,省略接口

](/2021/10/24/go-generic-eliding-interface/)

原创图书

Go并发编程一年回顾(2021) - 图1
Go并发编程一年回顾(2021) - 图2

分类

标签云

AndroidApacheBenchBowerC#CDNCQRSCRCCSSCompletableFutureComsatCuratorDSLDisruptorDockerEmberFastJsonFiberGAEGCGnuplotGoGradleGruntGulpHadoopHazelcastIPFSIgniteJVMJavaKafkaLambdaLinuxLongAdderMathJaxMavenMemcachedMetricsMongoNetty

归档

近期文章

友情链接

© 2021 smallnest
Powered by Hexo

首页 归档 github 网站群 Go 汇编示例 Go Web 开发示例 Go 数据库开发教程 RPCX 官网 RPC 开发指南 Scala 集合技术手册 关于

Go并发编程一年回顾(2021) - 图3
https://colobu.com/2021/11/09/the-state-of-go-sync-2021/