Go中的锁源码是如何实现Mutex的?

准确地说是信号量(semaphore, mutext是semaphore的一种)的实现方式有两种:wait的时候忙等待或者阻塞自己。

忙等待和阻塞方式各有优劣:
忙等待会使CPU空转,好处是如果在当前时间片内锁被其他进程释放,当前进程直接就能拿到锁而不需要CPU进行进程调度了。适用于锁占用时间较短的情况,且不适合于单处理器。
阻塞不会导致CPU空转,但是进程切换也需要代价,比如上下文切换,CPU Cache Miss。
下面看一下golang的源码里面是怎么实现锁的。golang里面的锁有两个特性:
1.不支持嵌套锁
2.可以一个goroutine lock,另一个goroutine unlock

这里要解释一下atomic.***pareAndSwapInt32(),atomic包是由golang提供的low-level的原子操作封装,主要用来解决进程同步为题,官方并不建议直接使用。我在上一篇文章中说过,操作系统级的锁的实现方案是提供原子操作,然后基本上所有锁相关都是通过这些原子操作来实现。***pareAndSwapInt32()就是int32型数字的***pare-and-swap实现。cas(&addr, old, new)的意思是if *addr==old, *addr=new。大部分操作系统支持CAS,x86指令集上的CAS汇编指令是CMPXCHG。下面我们继续看上面的lock函数。

首先先忽略race.Enabled相关代码,这个是go做race检测时候用的,这个时候需要带上-race,则race.Enabled被置为true。Lock函数的入口处先调用CAS尝试去获得锁,如果m.state==0,则将其置为1,并返回。

继续往下看,首先将m.state的值保存到old变量中,new=old|mutexLocked。直接看能让for退出的第三个if条件,首先调用CAS试图将m.state设置成new的值。然后看一下if里面,如果m.state之前的值也就是old如果没有被占用则表示当前goroutine拿到了锁,则break。我们先看一下new的值的变化,靠前个if条件里面new = old + 1<<mutexWaiterShift,结合上面的mutex的state各个位的意义,这句话的意思表示mutex的等待goroutine数目加1。还有awoke为true的情况下,要将m.state的标志位取消掉,也就是这句new &^= mutexWoken的作用。继续看第三个if条件里面,如果里面的if判断失败,则走到runtime_Semacquire()。

看一下这个函数runtime_Semacquire()函数,由于golang1.5之后把之前C语言实现的代码都干掉了,所以现在很低层的代码都是go来实现的。通过源码中的定义我们可以知道这个其实就是信号量的wait操作:等待*s>0,然后减1。编译器里使用的是sync_runtime.semacquire()函数。

seg 1代码片段semroot()返回结构体semaRoot。存储方式是先对信号量的地址做移位,然后做哈希(对251取模,这个地方为什么是左移3位和对251取模不太明白)。semaRoot相当于和mutex.sema绑定。看一下semaRoot的结构:一个sudog链表和一个nwait整型字段。nwait字段表示该信号量上等待的goroutine数目。head和tail表示链表的头和尾巴,同时为了线程安全,需要使用一个互斥量来保护链表。这个时候细心的同学应该注意到一个问题,我们前面不是从Mutex跟过来的吗,相当于Mutex的实现了使用了Mutex本身?实际上semaRoot里面的mutex只是内部使用的一个简单版本,和sync.Mutex不是同一个。现在把这些倒推回去,runtime_Semacquire()的作用其实就是semaphore的wait(&s):如果*s<0,则将当前goroutine塞入信号量s关联的goroutine waiting list,并休眠。

现在mutex.Lock()还剩下runtime_canSpin(iter)这一段,这个地方其实就是锁的自旋版本。golang对于自旋锁的取舍做了一些限制:1.多核; 2.GOMAXPROCS>1; 3.至少有一个运行的P并且local的P队列为空。golang的自旋尝试只会做几次,并不会一直尝试下去,感兴趣的可以跟一下源码。

函数入口处的四行代码和race detection相关,暂时不用管。接下来的四行代码是判断是否是嵌套锁。new是m.state-1之后的值。我们重点看for循环内部的代码。

这两句是说:如果阻塞在该锁上的goroutine数目为0或者mutex处于lock或者唤醒状态,则返回。

这里先将阻塞在mutex上的goroutine数目减一,然后将mutex置于唤醒状态。runtime_Semrelease和runtime_Semacquire的作用刚好相反,将阻塞在信号量上goroutine唤醒。有人可能会问唤醒的是哪个goroutine,那么我们可以看一下goroutine wait list的入队列和出队列代码。

如上所示,wait list入队是插在队尾,出队是从头出。
武汉网站制作www.zhandodo.com

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考;文章版权归原作者所有!本站作为信息内容发布平台,页面展示内容的目的在于传播更多信息;本站不提供任何相关服务,阁下应知本站所提供的内容不能做为操作依据。市场有风险,投资需谨慎!如本文内容影响到您的合法权益(含文章中内容、图片等),请及时联系本站,我们会及时删除处理。


为您推荐