Featured image of post Go内部锁

Go内部锁

Go语言内部Mutex和RWMutex学习笔记

Mutex

Mutex由 statesema 两个字段组成,state表示当前互斥锁的状态,sema是用于控制锁状态的信号量。

1
2
3
4
type Mutex struct {
	state int32		// 状态
	sema  uint32  // 信号量
}

互斥锁的状态的 低三位 分别表示互斥锁 是否锁定(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个字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type RWMutex struct {
    w           Mutex  // 控制 writer 在 队列 排队
    writerSem   uint32 // 写信号量,用于等待前面的 reader 完成读操作
    readerSem   uint32 // 读信号量,用于等待前面的 writer 完成写操作
    readerCount int32  // reader 的总数量,同时也指示是否有 writer 在队列中等待
    readerWait  int32  // writer 前面 reader 的数量
}

// 允许最大的 reader 数量
const rwmutexMaxReaders = 1 << 30

w:主要提供写锁相关的功能

readerCount:存储了正在执行的读操作的数量

readerWait:表示当前写操作被阻塞时前方等待的读操作的数量

写锁

加锁Lock():

  1. 首先会先调用w.Lock() 阻塞后续的写操作 。因为锁已经被获取,当其他的goroutine获取写锁时会进入 自旋或者休眠

  2. 然后通过 原子操作 (调用atomic.AddInt32方法)将readerCount设置为负数, 阻塞后续的读操作

  3. 然后判断如果有其他的goroutine持有读锁的话,当前goroutine会进入 休眠状态,等待所有读锁读完之后释放writerSem信号量唤醒当前goroutine。

  4. 先阻塞写锁后阻塞读锁避免读操作因为连续的写操作饿死。

释放UnLock():

  1. 先通过 原子操作 将(调用atomic.AddInt32方法)readerCount设置为正数,释放读锁。

  2. 然后通过for循环释放所有因为获取 读锁 而陷入等待的goroutine。

  3. 然后调用w.UnLock()释放写锁。

读锁

加锁RLock():

  1. 该方法会通过 原子操作 将(调用atomic.AddInt32方法)readerCount加一。

  2. 然后如果返回负数,说明其他goroutine获得了写锁,当前goroutine就会陷入 休眠 等待锁的释放。

  3. 如果返回非负数,说明没有goroutine获得写锁,该方法会返回成功。

释放RUnLock():

  1. 该方法会通过原子操作将(调用atomic.AddInt32方法)readerCount减一。

  2. 如果返回值大于零,直接解锁成功。

  3. 如果小于零,说明有一个写操作正在执行。这时会调用rUnlockSlow()方法,他会减少写操作前面等待的读操作的数量readerWait,并在 所有读操作完成之后 触发写操作的信号量writerSem唤醒写线程。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计