3

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

 2 years ago
source link: https://colobu.com/2021/11/09/the-state-of-go-sync-2021/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

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

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

不过这段代码的风格经常会被拿来吐槽吧,当然有一个理由就是这样写比较直观,大家学习代码风格和最佳实践的时候更可能这样去写:

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 {
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的字段变化

先前,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

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两个便利方法

如果使用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)

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK