Go并发编程一年回顾

16469d5878f032a3850eab6aa3116612.png

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

TryLock终于要发布

b3f967a2954253fdc2a5b47c8e166172.png

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

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

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

首先是Mutex.TryLock:

func (m *Mutex) TryLock() bool {
 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  return true
 }
 return false
}

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

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

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

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

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

func (rw *RWMutex) TryLock() bool {
 if !rw.w.TryLock() {
  return false
 }
 if !atomic.CompareAndSwapInt32(&rw.readerCount, 0, -rwmutexMaxReaders) {
        rw.w.Unlock()
  return false
 }
 return true
}

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

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

func (rw *RWMutex) TryRLock() bool {
 for {
  c := atomic.LoadInt32(&rw.readerCount)
  if c < 0 {
   return false
  }
  if atomic.CompareAndSwapInt32(&rw.readerCount, c, c+1) {
   return true
  }
 }
}

这段代码首先检查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的字段变化

6bf31fa9cc7481fa0df57f566b128b54.png

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

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

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

Pool中使用fastrandn替换fastrand

497b6c972fe943535789620bb6967a94.png

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

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

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

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

ada78cee08cd52da5f8b820a69199def.png

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

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

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

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

相关链接:

  1. https://colobu.com/2020/07/05/the-state-of-go-sync-2020/

  2. https://github.com/golang/go/issues/6123

  3. https://github.com/golang/go/issues/45435

  4. https://research.swtch.com/gomm

原文链接:https://colobu.com/2021/11/09/the-state-of-go-sync-2021/

Kubernetes线下培训

6b40f0930b3b3e7c98527a1f8f7ab040.png

本次培训在北京开班,基于最新考纲,理论结合实战,通过线下授课、考题解读、模拟演练等方式,帮助学员快速掌握Kubernetes的理论知识和专业技能,并针对考试做特别强化训练,让学员能从容面对CKA认证考试,使学员既能掌握Kubernetes相关知识,又能通过CKA认证考试,理论、实践、考证一网打尽,学员可多次参加培训,直到通过认证。点击下方图片或者阅读原文链接查看详情。

7167bbfa33ca229ff6fa9c95922d5c08.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值