golang源码阅读(1) - sync.Mutex | 互斥锁

以下分析基于golang版本1.11.4,对应源码 https://github.com/golang/go/blob/go1.11.4/src/sync/mutex.go

互斥锁使用CAS原子操作、信号量、自旋操作实现。

锁资源为一个int型变量标识,不同位有不同的作用。通过CAS对它做原子操作。

获取锁资源时,如果锁资源没有被其他协程持有,则直接获取成功。

如果获取失败,分为两种模式:正常模式和饥饿模式。

正常模式下,所有竞争者可通过自旋尝试获取锁。当自旋尝试超过一定数量还没获取到时,则阻塞等待。直到接收到锁资源持有者释放锁资源时发出的信号量。如果有多个竞争者,只有一个获取到该信号量。

首先,解释下为什么要用自旋。自旋的好处是不主动让出执行权,避免上下文切换。当然也不能无限制的自旋空跑,浪费CPU资源。
另外,自旋也是有条件的,比如必须是在硬件多核、当前程序开启多线程的环境,且至少还有其他运行的P并且当前P的队列为空。从而避免没必要的自旋以及自旋阻碍其它协程运行。

那么这里存在一个问题。如果锁资源释放的不及时,所有竞争者都平等的等待释放锁资源时的信号量,由于新的竞争者的协程很可能还在CPU上跑,并且这种协程可能有多个,大概率会比老的竞争者更容易抢到锁资源。这反而导致了锁竞争的不公平。

为此,有了饥饿模式。饥饿模式下,锁资源将按照竞争者的尝试获取锁资源的顺序来分配。(维护了一个先进先出的队列,锁资源直接给到队列头的竞争者。新的竞争者将直接插入队尾,不尝试获取锁资源也不会自旋。)

进入饥饿模式的条件是,尝试获取锁资源超过了1毫秒。
退出饥饿模式的条件有两种,任意满足一种就可以:要么是竞争者获取到锁后发现队列中没有其他竞争者了,要么是竞争者获取到锁的用时不到1毫秒。

最后,我们来比较下正常模式和饥饿模式,其实上面都已经说到了。
正常模式由于有自旋操作,可以避免上下文切换。但是可能出现不公平导致某些竞争者等待时间过长。
饥饿模式解决了一些公平性问题,但是由于没有自旋,可能会多一些上下文切换。

设计互斥锁,其实是在多种场景下(竞争者数量、持锁时间等因素),程序性能、硬件资源使用率间做trade off。

参考链接:

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/64224/

0%