基于本文回答
0
评论

Go sync.Mutex(互斥锁)有哪两种模式?(正常模式 vs 饥饿模式)

知识点图片

Go 语言的 sync.Mutex 在设计上为了平衡性能(吞吐量)公平性,实现了两种模式:正常模式(Normal Mode)饥饿模式(Starvation Mode)

这两种模式是自动切换的,以下是详细对比:


1. 正常模式 (Normal Mode)

这是 Mutex 的默认模式,主要为了性能优化。

  • 工作机制:

    • 当一个 Goroutine 释放锁时,它会唤醒等待队列(FIFO)头部的 Goroutine。
    • 关键点(抢占): 被唤醒的 Goroutine 不会直接拥有锁,而是需要和新来的 Goroutine 竞争。
    • 新来的优势: 新来的 Goroutine 正在 CPU 上运行(处于活跃状态),而刚被唤醒的 Goroutine 需要上下文切换(从睡眠中醒来)。因此,新来的 Goroutine 往往能抢到锁。
    • 自旋(Spinning): 新来的 Goroutine 如果发现锁被占用,会尝试“自旋”(空转 CPU 等待一小会儿),如果在这期间锁释放了,它就能立刻拿到,避免了昂贵的系统调用和上下文切换。
  • 优点:

    • 吞吐量极高。减少了 Goroutine 的上下文切换和系统调用开销。
  • 缺点:

    • 不公平。已经在排队的 Goroutine 可能会一直抢不到锁(因为总有新来的插队),导致尾部延迟(Tail Latency)甚至饿死

2. 饥饿模式 (Starvation Mode)

这是为了解决“尾部延迟”和“饿死”问题而引入的模式,主要为了公平性

  • 工作机制:

    • 严格 FIFO: 锁的所有权会从释放锁的 Goroutine 直接移交(Handoff)给等待队列头部的 Goroutine。
    • 禁止抢占: 新来的 Goroutine 不会尝试自旋,也不会尝试去抢锁。
    • 乖乖排队: 新来的 Goroutine 会直接加入到等待队列的尾部。
  • 优点:

    • 绝对公平。保证了等待时间最长的 Goroutine 能够优先获得锁。
    • 消除了长尾延迟。
  • 缺点:

    • 性能较低。因为禁用了自旋,且强制进行上下文切换,导致 CPU 缓存亲和性(Cache Locality)变差,整体吞吐量下降。

3. 模式切换条件

Go 运行时会根据情况自动在两种模式间切换:

A. 从 正常模式 -> 切换到 -> 饥饿模式

触发条件:
如果一个 Goroutine 在等待队列中等待超过 1 毫秒(1ms) 还没有获取到锁,Mutex 就会切换到饥饿模式。

B. 从 饥饿模式 -> 切换回 -> 正常模式

触发条件(满足其一即可):

  1. 队列空了: 获得锁的 Goroutine 是等待队列中的最后一个(即它后面没有人在排队了)。
  2. 等待时间短: 获得锁的 Goroutine 等待时间小于 1 毫秒

总结对比表

特性 正常模式 (Normal) 饥饿模式 (Starvation)
侧重点 性能 / 吞吐量 公平性 / 延迟控制
锁获取方式 竞争获取(新来的 vs 唤醒的) 直接移交(Handoff)给队头
新 Goroutine 行为 尝试自旋,尝试抢锁(插队) 不自旋,直接去队尾排队
适用场景 锁竞争不激烈,持有锁时间短 锁竞争激烈,且有 Goroutine 等待过久
默认状态

核心设计哲学

Go 的 Mutex 设计认为:通常情况下,性能优先于公平。但在极端情况下(有人等太久),必须牺牲性能来保证公平,防止程序逻辑死锁或响应超时。

右滑查看面试常问