去年的时候我写了一篇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
2
3
4
5
6
func (m *Mutex) TryLock() bool {
if atomic.CompareAndSwapInt32(&m.state, 0 , mutexLocked) {
return true
}
return false
}
也就是利用aromic.CAS操作state字段,如果当前没有被锁或者没有等待锁的情况,就可以成功获取到锁。不会尝试spin和与等待者竞争。
不要吐槽上面的代码风格,可能你觉得不应该写成下面的方式吗?原因在于我删除了race代码,那些代码块中包含race代码,所以不能像下面一样简写:
1
2
3
func (m *Mutex) TryLock() bool {
return atomic.CompareAndSwapInt32(&m.state, 0 , mutexLocked)
}
读写锁有些麻烦,因为它有读锁和写锁两种情况。
首先看RWMutex.TryLock(去除了race代码):
1
2
3
4
5
6
7
8
9
10
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代码):
1
2
3
4
5
6
7
8
9
10
11
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的字段变化
先前,WaitGroup类型使用[3]uint32
作为state1
字段的类型,在64位和32位编译器情况下,这个字段的byte的意义是不同的,主要是为了对齐。虽然使用一个字段很"睿智",但是阅读起来却很费劲,现在,Go team把它改成了两个字段,根据对齐规则,64位编译器会对齐相应字段,讲真的,我们不差那4个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
快很多,相关的文章可以看下面中的注释中的地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//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 += 0 xa0761d6478bd642f
hi, lo := math.Mul64(mp.fastrand, mp.fastrand^0 xe7037ed1a0b428db)
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,这两个方法的逻辑经常会用到,现在这两个方法已经添加到标准库中了。
1
2
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的并发相关的库比较稳定,并没有大的变化。