互斥锁正常模式和饥饿模式有什么区别 ?

参考回答

在 Golang 中,互斥锁(sync.Mutex)有两种运行模式:正常模式饥饿模式。两种模式的主要区别体现在对锁竞争的策略上,分别侧重于性能和公平性。

  1. 正常模式
    • 默认模式,侧重于高性能。
    • Goroutine 争用锁时,不保证公平性,倾向于让新来的 Goroutine 有机会直接抢锁,避免上下文切换带来的性能开销。
    • 如果锁竞争激烈,可能导致某些 Goroutine 长时间无法获取锁。
  2. 饥饿模式
    • 为了解决某些 Goroutine 长时间获取不到锁的问题,饥饿模式强调公平性。
    • 锁的所有权严格按照 FIFO 队列分配,锁被释放后优先交给队列中等待最久的 Goroutine。
    • 饥饿模式的代价是性能下降,因为频繁的 Goroutine 切换增加了开销。

详细讲解与拓展

1. 正常模式的工作机制

  • 在正常模式下,当一个 Goroutine 尝试获取锁时:
    • 如果锁是 可用状态,直接获取。
    • 如果锁是 已被持有状态,当前 Goroutine 会被放入等待队列并阻塞,等待锁释放。
    • 一旦锁被释放,Go 调度器会随机选择一个等待的 Goroutine 尝试获取锁。
  • 特点:
    • 更倾向于让新来的 Goroutine 尝试获取锁,而不是立即唤醒队列中的 Goroutine。
    • 优化了性能,减少了线程上下文切换的开销。
  • 问题:
    • 如果锁竞争激烈(很多 Goroutine 争用锁),可能会出现 锁倾斜 现象:某些 Goroutine 长时间无法获取锁,导致“饥饿”。

2. 饥饿模式的工作机制

  • 饥饿模式是在正常模式无法满足公平性时引入的一种机制。
  • 工作流程:
    1. 当一个 Goroutine 在等待队列中等待锁的时间过长(Go 的实现中设定为 1ms),锁就会进入饥饿模式。
    2. 饥饿模式下,锁严格按照队列顺序分配,释放锁的 Goroutine 会优先唤醒队列中第一个等待的 Goroutine。
    3. 新的 Goroutine 请求锁时不会直接抢占,而是加入队列等待。
  • 特点:
    • 保证了锁分配的公平性,避免了 Goroutine 长时间得不到锁的问题。
    • 增加了性能开销(频繁的上下文切换和 Goroutine 调度)。

3. 两种模式的切换条件

  1. 从正常模式切换到饥饿模式
    • 如果等待锁的 Goroutine 时间超过 1ms,锁会进入饥饿模式。
  2. 从饥饿模式切换到正常模式
    • 当锁被释放并且等待队列为空时,锁会回到正常模式。

4. 举例分析

以下是一个模拟锁竞争的代码示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            mu.Lock()
            fmt.Printf("Goroutine %d acquired the lock\n", id)
            time.Sleep(100 * time.Millisecond) // 模拟任务
            mu.Unlock()
        }(i)
    }

    wg.Wait()
}

在高竞争情况下:
– 如果是 正常模式,新来的 Goroutine 可能优先抢锁,可能会导致某些 Goroutine 长时间等待。
– 如果切换到 饥饿模式,锁会按照队列顺序严格分配,避免长时间等待。


总结

  1. 正常模式
    • 优先考虑性能,允许新 Goroutine 直接抢锁。
    • 适合锁争用不激烈的场景。
  2. 饥饿模式
    • 强调公平性,按照队列顺序分配锁。
    • 适合锁争用非常激烈的场景,防止某些 Goroutine 长时间得不到锁。

两种模式的切换保证了锁既能高效运行,又能避免极端情况下的资源饥饿问题。理解两种模式的差异,有助于开发者在高并发场景中更好地设计和优化程序。

发表评论

后才能评论