Golang死锁排查:五哲学家问题wg.Wait()偶现goroutine全部休眠错误
问题根因分析
- 互斥锁值传递问题
sync.Mutex是值类型,原始代码中eat函数的left、right参数是值传递,每个协程拿到的是锁的副本,加锁操作完全不生效,没有互斥效果,所有哲学家可以同时拿到左右筷子,全部阻塞在等待host授权。你后续改成传引用解决了这个问题,但还有其他潜在问题。 - 随机数生成逻辑错误
RandBool函数每次调用都执行rand.Seed(time.Now().UnixNano()),多个协程启动时间接近时,UnixNano时间戳相同,会导致所有协程生成的随机数一致,所有哲学家拿锁的顺序完全相同,大概率出现所有人同时拿到左锁、等待右锁的情况,直接触发死锁。 - 无缓冲通道的竞态阻塞
permissionChannel是无缓冲通道,host发送授权时必须有哲学家刚好在接收才能发送成功。如果此时所有哲学家都在抢筷子锁,没有协程读通道,host的发送操作会阻塞,同时哲学家拿完锁后等不到授权,形成循环阻塞,触发死锁。你将通道改成带缓冲的之后,host可以提前把授权存入缓冲区,不需要阻塞等待接收,降低了竞态概率,所以问题暂时得到解决,但没有根除死锁风险。 - 执行顺序不合理
原始代码的逻辑是先拿筷子锁→再等授权,拿着稀缺的筷子资源等待授权,会导致资源浪费,进一步提升死锁概率。
完整修复方案
package main import ( "fmt" "math/rand" "sync" "time" ) const ( numOfPhilosophers = 5 numOfMeals = 3 maxEaters = 2 ) func init() { // 全局只初始化一次随机种子 rand.Seed(time.Now().UnixNano()) } func main() { chopsticks := make([]*sync.Mutex, numOfPhilosophers) for i := 0; i < numOfPhilosophers; i++ { chopsticks[i] = &sync.Mutex{} } permissionChannel := make(chan bool, maxEaters) // 缓冲大小设为最大同时就餐数即可 finishEating := make(chan bool) var wg sync.WaitGroup // 启动host协程 go permissionFromHost(permissionChannel, finishEating) wg.Add(numOfPhilosophers) for i := 1; i <= numOfPhilosophers; i++ { // 锁传指针,避免值拷贝 go eat(i, chopsticks[i-1], chopsticks[i%numOfPhilosophers], &wg, permissionChannel, finishEating) } wg.Wait() close(finishEating) // 通知host协程退出 } func eat(philosopherId int, left *sync.Mutex, right *sync.Mutex, wg *sync.WaitGroup, permissionChannel <-chan bool, finishEatingChannel chan<- bool) { defer wg.Done() for i := 1; i <= numOfMeals; i++ { fmt.Printf("philosopher %d waiting for permission from host\n", philosopherId) // 先拿授权,再拿筷子,避免拿着资源等授权 <-permissionChannel // 随机顺序拿锁 if RandBool() { left.Lock() right.Lock() } else { right.Lock() left.Lock() } fmt.Printf("starting to eat %d (time %d)\n", philosopherId, i) time.Sleep(time.Millisecond * 10) // 模拟就餐耗时 fmt.Printf("finish to eat %d (time %d)\n", philosopherId, i) // 先释放筷子 left.Unlock() right.Unlock() // 通知host就餐完成 finishEatingChannel <- true } } func permissionFromHost(permissionChannel chan<- bool, finishEating <-chan bool) { ctr := 0 for { select { case <-finishEating: ctr-- case permissionChannel <- true: if ctr < maxEaters { ctr++ } } } } func RandBool() bool { return rand.Intn(2) == 1 }
修复说明
- 互斥锁改为指针传递,保证锁的互斥性生效
- 调整执行顺序为先等授权→再拿筷子,避免资源浪费
- 随机种子全局只初始化一次,保证拿锁顺序的随机性,避免所有哲学家拿锁顺序一致导致的死锁
permissionChannel缓冲大小设置为最大同时就餐数即可,不需要设置为哲学家数量,减少资源浪费- 增加host协程的退出机制,所有哲学家吃完后关闭
finishEating通道即可触发host协程退出
内容的提问来源于stack exchange,提问作者Gilo
相关产品推荐
相关产品推荐

