Go Mutex 饥饿模式

900 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

相关概念:

原子操作: 指那些不能够被打断的操作被称为原子操作,当有一个CPU在访问这块内容地址时,其他CPU就不能访问。

互斥锁: 挂起(通过休眠来使进程阻塞)

自旋锁: 指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断锁能否够被成功获取,直到拿到锁才会退出循环。获取锁的线程持续活跃,不挂起(不是通过休眠来使进程阻塞),继续占有cpu。

饥饿问题: 一些悲惨的G长时间获取不到锁,导致业务逻辑不能继续完整执行。而当前正在cpu上运行的goroutine可能会更先获取到锁,比如自旋锁。

描述居中对齐
正常模式所有goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时进行锁获取,通常新请求锁的goroutine更容易获取锁(持续占有cpu),被唤醒的goroutine则不容易获取到锁
饥饿模式所有尝试获取锁的goroutine进行等待排队,新请求锁的goroutine不会进行锁获取(禁用自旋),而是加入队列尾部等待获取锁

饥饿模式:

解决问题: 主要解决了等待G队列的长尾问题(先进先出队列尾部的等待者一直无法获取到 mutex 的情况),防止G被饿死。但性能较低(由于新进入的活跃G起初处于自旋状态(消耗CPU资源),所以避免了G的切换调度)

执行过程: 饥饿模式下,直接由unlock把锁交给等待队列中排在第一位的G,同时,饥饿模式下,新进来的G不会参与抢锁也不会进入自旋状态(禁用自旋),会直接进入等待队列的尾部排队。

触发条件: (1) 当一个G等待锁时间超过“1毫秒”时,Mutex切换到饥饿模式。

取消条件:

  1. 当一个G获取到锁且在等待队列的末尾(等待队列的所有任务执行完了),那么Mutex切换回正常模式
  2. 这个G获取锁的等待时间在1ms内,那么Mutex切换回正常模式

Mutex结构: image.png

type Mutex struct {
    state int32  // 表示锁当前的状态
    sema  uint32 // 信号量 用于向处于Gwaitting的G发送信号
}
// 状态值:
sema是个信号量,用来唤醒goroutine,初始为0,用于判断是否有可用资源。没有则一直等待。
state是一个4字节(32位)的变量,由于4部分组成,是锁的本体
1. 0位判断当前锁是否上锁(锁定标志位,0:未锁定 1:锁定)
2. 1位判断当前锁是否是被其他goroutine唤醒的(唤醒标志位,0:未唤醒 1:唤醒)
3. 2位判断当前锁是否处于饥饿状态
4. 3-31位用于计算当前等待的goroutine数量

关于锁的使用建议:

  1. 写业务时不能全局使用同一个 Mutex
  2. 千万不要将要加锁和解锁分到两个以上 Goroutine 中进行(容易形成死锁)
  3. Mutex 千万不能被复制(包括不能通过函数参数传递),否则会复制传参前锁的状态:已锁定 or 未锁定。很容易产生死锁,关键是编译器还发现不了这个 Deadlock~
  4. 尽量避免使用 Mutex,如果非使用不可,尽量多声明一些 Mutex,采用取模分片的方式去使用其中一个 Mutex(分段锁)(尽量减小锁的颗粒度)

更多可参考大佬文章: