Peterson算法实现出现疑似死锁,疑与编译器优化相关
Peterson算法在Go中出现假死问题的原因与解决方法
你的判断完全正确——这个假死问题就是编译器优化+CPU缓存一致性导致的,和所谓的“良性数据竞争”风险直接相关。
问题根源
Peterson算法的正确性依赖于三个关键操作的内存可见性和执行顺序:
- 线程设置自己的
flag为true - 设置
victim为自己的ID - 循环检查对方的
flag和当前victim值
但在Go中,没有同步机制的情况下,编译器可能会对指令进行重排(比如把m.victim = id移到m.flag[id] = true之前),或者CPU会将变量缓存到寄存器/本地缓存中,导致一个goroutine对flag或victim的修改,另一个goroutine无法及时看到。最终循环条件一直为true,出现无限停滞的假死状态。
而当你用原子操作存储/加载victim时,原子操作自带内存屏障,强制了内存可见性和指令顺序,所以问题消失。
解决方法
1. 临时禁用编译器优化(不推荐用于生产)
如果只是验证问题,可以通过编译选项禁用优化和内联:
go run -gcflags="-N -l" your_program.go
-N 禁用所有编译器优化,-l 禁用函数内联。但这只是临时方案,因为即使禁用编译器优化,CPU的缓存一致性问题依然可能导致可见性问题,无法从根本上保证正确性。
2. 用内存同步机制修复算法(推荐)
Go没有volatile关键字,正确的做法是用sync/atomic包来保证共享变量的内存可见性和原子性。修改后的代码如下:
package main import ( "fmt" "sync" "sync/atomic" "runtime" ) type mutex struct { flag [2]atomic.Bool victim atomic.Int32 } func (m *mutex) lock(id int) { m.flag[id].Store(true) m.victim.Store(int32(id)) // 循环检查:对方flag为true,且victim是当前id时等待 for m.flag[1-id].Load() && m.victim.Load() == int32(id) { runtime.Gosched() // 让出CPU,避免空耗资源 } } func (m *mutex) unlock(id int) { m.flag[id].Store(false) } func main() { var wg sync.WaitGroup var mu mutex var cpt atomic.Int32 n := 100000 for i := 0; i < 2; i++ { wg.Add(1) go func(id int) { defer wg.Done() for j := 0; j < n; j++ { mu.lock(id) cpt.Add(1) mu.unlock(id) } }(i) } wg.Wait() fmt.Println(cpt.Load()) }
这里做了两个关键修改:
- 把
flag和victim换成原子类型,所有读写都用Load()和Store()操作,这些操作会插入内存屏障,保证修改对其他goroutine可见,同时阻止指令重排。 - 计数器
cpt也换成了atomic.Int32,彻底消除了原本的数据竞争(之前计数器值正确只是侥幸,数据竞争会导致未定义行为)。
另外,空循环等待时加入runtime.Gosched(),让当前goroutine让出CPU,避免空耗资源。
内容的提问来源于stack exchange,提问作者Taki Eddine MEKHALFA
相关产品推荐
相关产品推荐

