并发原语
http://laiyong.wang/2024/06/24/concurrentPrimitive/
一、饥饿模式的设计背景
1. 问题场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func starvationDemo() { var mu sync.Mutex go func() { for { mu.Lock() time.Sleep(10 * time.Microsecond) mu.Unlock() } }()
go func() { mu.Lock() defer mu.Unlock() fmt.Println("关键操作") }() }
|
问题:新到达的协程可能通过自旋快速抢锁,导致早期等待的协程长期饥饿
二、饥饿模式的实现机制
1. Mutex 状态字段结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type Mutex struct { state int32 sema uint32 }
const ( mutexLocked = 1 << iota mutexWoken mutexStarving mutexWaiterShift = iota )
|
2. 模式切换条件
转换方向 |
触发条件 |
正常→饥饿 |
协程等待锁的时间超过 1ms |
饥饿→正常 |
当前等待队列为空 或 持有锁的协程是队列中最后一个等待者(饥饿模式完成使命) |
三、饥饿模式下的特殊行为
1. 锁分配策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func (m *Mutex) lockSlow() { if starving { runtime_SemacquireMutex(&m.sema, true) if m.state>>mutexWaiterShift == 0 { m.state &^= mutexStarving } return } }
|
2. 行为变化对比
特性 |
正常模式 |
饥饿模式 |
自旋尝试 |
允许(最多4次) |
禁止 |
唤醒顺序 |
新请求可能插队 |
严格FIFO队列 |
性能目标 |
低延迟 |
公平性 |
适用场景 |
短期锁竞争 |
长期锁竞争 |
四、实战场景分析
场景1:公平性保障
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func fairnessTest() { var mu sync.Mutex go func() { for i := 0; i < 1000; i++ { mu.Lock() time.Sleep(100 * time.Microsecond) mu.Unlock() } }()
go func() { start := time.Now() mu.Lock() defer mu.Unlock() fmt.Printf("等待耗时: %v\n", time.Since(start)) }() }
|
结果:饥饿模式保证长期等待的协程在 1ms 后获得锁
场景2:模式切换验证
1 2 3 4 5
| GODEBUG=mutexdetail=1 go run main.go
Mutex state: locked=1 starving=1 waiters=3
|
五、性能影响与优化建议
1. 性能指标对比
指标 |
正常模式 |
饥饿模式 |
吞吐量 |
高 |
降低 30-40% |
最大延迟 |
不可控 |
<2ms |
2. 优化策略
• 减少临界区耗时:确保锁内代码执行时间 <100μs
• 控制并发密度:使用工作池限制并发数
1 2 3 4 5 6 7 8 9
| sem := make(chan struct{}, 100) for task := range tasks { sem <- struct{}{} go func() { defer func() { <-sem }() process(task) }() }
|
通过饥饿模式的引入,Go 的 sync.Mutex
在保持高性能的同时,有效避免了极端情况下的协程饥饿问题。开发应结合具体场景:
• 对延迟敏感的场景控制临界区执行时间
• 对公平性敏感的场景可主动设置 Lock
的超时机制
• 高频竞争场景建议改用 channel
或 atomic
操作