去年的时候我写了一篇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:
|
|
也就是利用aromic.CAS操作state字段,如果当前没有被锁或者没有等待锁的情况,就可以成功获取到锁。不会尝试spin和与等待者竞争。
不要吐槽上面的代码风格,可能你觉得不应该写成下面的方式吗?原因在于我删除了race代码,那些代码块中包含race代码,所以不能像下面一样简写:
|
|
读写锁有些麻烦,因为它有读锁和写锁两种情况。
首先看RWMutex.TryLock(去除了race代码):
|
|
首先底层的Mutex.TryLock,尝试获取w字段的锁,如果成功,需要检查当前的Reader, 如果没有reader,则成功, 如果此时不幸还有reader没有释放读锁,那么尝试Lock也是不成功的,返回false。注意返回之前一定要把rw.w的锁释放掉。
接下来看RWMutex.TryRLock(去除了race代码):
|
|
这段代码首先检查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个字节。
|
|
64位对齐情况下state1和state2意义很明确,如果不是64位对齐,还得巧妙的转换一下。
Pool中使用fastrandn替换fastrand
Go运行时中提供了fastrandn
方法,要比fastrand() % n
快很多,相关的文章可以看下面中的注释中的地址。
|
|
所以sync.Pool中使用fastrandn
做了一点点修改,用来提高性能。好卷啊,这一点点性能都来压榨,关键,这还是开启race才会执行的代码。
sync.Value增加了Swap和CompareAndSwap两个便利方法
如果使用sync.Value,这两个方法的逻辑经常会用到,现在这两个方法已经添加到标准库中了。
|
|
Go 1.18中虽然实现了泛型,但是一些库的修改有可能在将来的版本中实现了。在泛型推出来之后,atomic对类型的支持会有大大的加强,所以将来Value这个类型有可能退出历史舞台,很少被使用了。(参考Russ Cox的文章Updating the Go Memory Model)
整体来说,Go的并发相关的库比较稳定,并没有大的变化。