Mutex
Mutex由 state 和 sema 两个字段组成,state表示当前互斥锁的状态,sema是用于控制锁状态的信号量。
|
|
互斥锁的状态的 低三位 分别表示互斥锁 是否锁定(mutexLocked)(1是锁定)、 是否有唤醒的goroutine(mutexWoken)(1是有)、当前线程 是否是饥饿状态(mutexStarving)(1是饥饿)。前面的就表示当前互斥锁上等待的goroutine的个数。
Mutex的状态
Mutex有两种模式,正常模式和饥饿模式。
在正常模式下,锁的等待者会按照 先进先出 的顺序去获取锁,但是刚被唤醒的goroutine与新创建的goroutine竞争时,大概率获取不到锁。为了减少这种情况的发生,一旦goroutine超过1ms没有获取到锁,他就会将当前互斥锁切换到饥饿模式,防止部分goroutine被饿死。
在饥饿模式下,互斥锁会直接交给 等待队列最前面 的goroutine。新的goroutine在该状态下不能获取锁,也不会进入自旋状态,他只会在 队列末尾 等待。如果一个goroutine获得了互斥锁并且他在队列末尾或者他等待的时间少于 1ms ,那么当前互斥锁就会切换为 正常状态。
正常状态能提供更好的性能,饥饿模式能够避免goroutine由于陷入等待无法获取锁造成的高延时。
加锁和解锁
加锁:在lock方法中,他会先通过CAS(CompareAndSwapInt32方法)去判断能否获取到锁,获取到的话就会将mutexLocked设置为1。如果获取时他的状态不是0,也就会获取不到锁,他会调用lockslow方法来尝试通过 自旋 来等待锁的释放。当前线程进入自旋的话会一直 保持CPU的占用,持续检查某些条件是否为真。处理完自旋相关的逻辑之后,互斥锁会根据上下文计算当前互斥锁的最新状态。然后使用CAS(CompareAndSwapInt32方法)更新状态,如果获取不到会通过 信号量 来保证资源不被两个goroutine获取。
解锁:解锁会先使用atomic.addint32()方法来快速解锁,如果成功就释放,不成功的话会进行慢速解锁(unlockslow方法),他会先 校验锁的状态,如果已经解锁了,会报错。然后正常情况下分为正常模式和饥饿模式处理,正常模式下如果有等待者,会唤醒等待者并移交锁的所有权,没有等待者的话直接返回。在饥饿模式下,他会将当前锁交给下一个正在尝试获取锁的等待者,然后依然是饥饿状态。
RWMutex
RWMutex结构体中共有5个字段。
|
|
w:主要提供写锁相关的功能
readerCount:存储了正在执行的读操作的数量
readerWait:表示当前写操作被阻塞时前方等待的读操作的数量
写锁
加锁Lock():
-
首先会先调用w.Lock()
阻塞后续的写操作。因为锁已经被获取,当其他的goroutine获取写锁时会进入自旋或者休眠。 -
然后通过
原子操作(调用atomic.AddInt32方法)将readerCount设置为负数,阻塞后续的读操作。 -
然后判断如果有其他的goroutine持有读锁的话,当前goroutine会进入
休眠状态,等待所有读锁读完之后释放writerSem信号量唤醒当前goroutine。 -
先阻塞写锁后阻塞读锁避免读操作因为连续的写操作饿死。
释放UnLock():
-
先通过
原子操作将(调用atomic.AddInt32方法)readerCount设置为正数,释放读锁。 -
然后通过for循环释放所有因为获取
读锁而陷入等待的goroutine。 -
然后调用w.UnLock()释放写锁。
读锁
加锁RLock():
-
该方法会通过
原子操作将(调用atomic.AddInt32方法)readerCount加一。 -
然后如果返回负数,说明其他goroutine获得了写锁,当前goroutine就会陷入
休眠等待锁的释放。 -
如果返回非负数,说明没有goroutine获得写锁,该方法会返回成功。
释放RUnLock():
-
该方法会通过原子操作将(调用atomic.AddInt32方法)readerCount减一。
-
如果返回值大于零,直接解锁成功。
-
如果小于零,说明有一个写操作正在执行。这时会调用rUnlockSlow()方法,他会减少写操作前面等待的读操作的数量readerWait,并在
所有读操作完成之后触发写操作的信号量writerSem唤醒写线程。