互斥锁正常模式和饥饿模式有什么区别 ?
参考回答
在 Golang 中,互斥锁(sync.Mutex)有两种运行模式:正常模式 和 饥饿模式。两种模式的主要区别体现在对锁竞争的策略上,分别侧重于性能和公平性。
- 正常模式:
- 默认模式,侧重于高性能。
- Goroutine 争用锁时,不保证公平性,倾向于让新来的 Goroutine 有机会直接抢锁,避免上下文切换带来的性能开销。
- 如果锁竞争激烈,可能导致某些 Goroutine 长时间无法获取锁。
- 饥饿模式:
- 为了解决某些 Goroutine 长时间获取不到锁的问题,饥饿模式强调公平性。
- 锁的所有权严格按照 FIFO 队列分配,锁被释放后优先交给队列中等待最久的 Goroutine。
- 饥饿模式的代价是性能下降,因为频繁的 Goroutine 切换增加了开销。
详细讲解与拓展
1. 正常模式的工作机制
- 在正常模式下,当一个 Goroutine 尝试获取锁时:
- 如果锁是 可用状态,直接获取。
- 如果锁是 已被持有状态,当前 Goroutine 会被放入等待队列并阻塞,等待锁释放。
- 一旦锁被释放,Go 调度器会随机选择一个等待的 Goroutine 尝试获取锁。
- 特点:
- 更倾向于让新来的 Goroutine 尝试获取锁,而不是立即唤醒队列中的 Goroutine。
- 优化了性能,减少了线程上下文切换的开销。
- 问题:
- 如果锁竞争激烈(很多 Goroutine 争用锁),可能会出现 锁倾斜 现象:某些 Goroutine 长时间无法获取锁,导致“饥饿”。
2. 饥饿模式的工作机制
- 饥饿模式是在正常模式无法满足公平性时引入的一种机制。
- 工作流程:
- 当一个 Goroutine 在等待队列中等待锁的时间过长(Go 的实现中设定为 1ms),锁就会进入饥饿模式。
- 饥饿模式下,锁严格按照队列顺序分配,释放锁的 Goroutine 会优先唤醒队列中第一个等待的 Goroutine。
- 新的 Goroutine 请求锁时不会直接抢占,而是加入队列等待。
- 特点:
- 保证了锁分配的公平性,避免了 Goroutine 长时间得不到锁的问题。
- 增加了性能开销(频繁的上下文切换和 Goroutine 调度)。
3. 两种模式的切换条件
- 从正常模式切换到饥饿模式:
- 如果等待锁的 Goroutine 时间超过 1ms,锁会进入饥饿模式。
- 从饥饿模式切换到正常模式:
- 当锁被释放并且等待队列为空时,锁会回到正常模式。
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 长时间等待。
– 如果切换到 饥饿模式,锁会按照队列顺序严格分配,避免长时间等待。
总结
- 正常模式:
- 优先考虑性能,允许新 Goroutine 直接抢锁。
- 适合锁争用不激烈的场景。
- 饥饿模式:
- 强调公平性,按照队列顺序分配锁。
- 适合锁争用非常激烈的场景,防止某些 Goroutine 长时间得不到锁。
两种模式的切换保证了锁既能高效运行,又能避免极端情况下的资源饥饿问题。理解两种模式的差异,有助于开发者在高并发场景中更好地设计和优化程序。