golang mutex原理

最近面试遇到问锁的问题,答得不是很好,重新做一下总结梳理

 go中的sync包提供了两种锁的类型,分别是互斥锁sync.Mutex和读写锁sync.RWMutex,这两种锁都属于悲观锁

饥饿模式与正常模式

在下面的内容会经常涉及到一个概念,饥饿模式,这里先简单说一下

1. 正常模式(非公平锁)

正常模式下,所有等待的 goroutine 按照先进先出的顺序等待。唤醒的 goroutine 不会直接拥有锁,而是和新请求锁的 goroutine 竞争锁。新请求的 goroutine 更容易获取锁,因为他在 CPU 上执行,而被唤醒的goroutine 需要重新获取CPU权限,再进行竞争。如此进行下去,就会导致 goroutine 长时间不能获取到锁。

2.饥饿模式(公平锁)

饥饿模式下,维持一个队列,所有的 goroutine 不再竞争,直接把锁交给队列中排在第一位的 goroutine;同时,饥饿模式下,新进来的 goroutine 不会进入到自旋状态,直接放在等待队列的尾部。饥饿模式的触发条件:

  • 一个 goroutine 等待锁唤醒的超过1ms;

  • 当前队列只剩下一个 goroutine 的时候,mutex 切换到饥饿模式。

sync.Mutex

提供方法

提供了三个方法:

  • Lock():进行加锁操作,在同一个goroutine中必须在锁释放之后才能进行再次上锁,不然会panic

  • Unlock(): 进行解锁操作,如果这个时候未加锁会panic,mutex和goroutine不关联,也就是说对于mutex的加锁解锁操作可以发生在多个goroutine间

  • tryLock():用得少,尝试获取锁,获取成功返回true,否则返回false,返回false可能有几种情况

    • 当锁被其他goroutine占有,无法获取,将立刻返回false,

    • 锁处于饥饿模式,此时要让饥饿队列先获取锁,将立刻返回false,

    • 当锁可用时尝试获取锁,获取失败也返回false

底层数据结构

底层就两个变量,分别说明锁的状态,以及一个信号量,用于唤醒等待goroutine

type Mutex struct {
   state int32        // 表示当前互斥锁的状态,复合型字段
   sema  uint32        // 信号量变量,用来控制等待goroutine的阻塞休眠和唤醒
}

state 是复合字段,不止说明了是否上锁,还说明了锁的模式,等待goroutine的数量等

const (
   mutexLocked = 1 << iota // mutex is locked
   mutexWoken
   mutexStarving
   mutexWaiterShift = iota
}
  • mutexLocked**:是否被锁定,由于 iota 的初始值是 0,因此 mutexLocked 的值为 1 << 0,即 1(二进制为 0001)。

  • mutexWoken**:这一位被设置时,表示有一个等待的 Goroutine 已经被唤醒或正在被唤醒。这个状态用于避免不必要地唤醒多个 Goroutine。值为 1 << 1**,即 2**(二进制为 0010**)

  • mutexStarving**:这一位被设置时,表示 Mutex 处于“饥饿模式”,这在某些特定条件下会被触发,下文将说到,值为 1 << 2,即 4(二进制为 0100

  • mutexWaiterShift**:表示等待获取锁的goroutine数量,从偏移量4开始计算

加锁过程

通过底层的atomic包中提供的CAS方法修改锁的状态,

  • 如果锁的状态是0,则改变其状态,然后直接返回

  • 否则进入 Slow path,看是否自旋或者在饥饿场景下获取锁

func (m *Mutex) Lock() {
   // Fast path: grab unlocked mutex.
   if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
      if race.Enabled {
         race.Acquire(unsafe.Pointer(m))
      }
      return
   }
   // Slow path (outlined so that the fast path can be inlined)
   m.lockSlow()
}

进入lockSlow函数后,会发现有一个for循环,这个时候实际上就是在自旋获取锁, 即:乐观的认为当前正在持有锁的g能在短时间内归还锁,所以需要一些条件来判断:到底能不能短时间归还

主要工作就是:

  1. 判断锁是否是饥饿状态,不是则自旋(即进行此次循环),否则直接进入队列。

  2. **如果自旋过程中等待时间过长,让锁自动变为饥饿模式**(网上说的1ms,没找到源码具体体现在哪)

下面贴部分代码,注释写的很详细,没截下来的地方,才是自旋,下面这部分图只是在判断某些条件

posted @ 2024-08-25 13:11  励志成为蔡徐坤  阅读(35)  评论(0编辑  收藏  举报