You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang死锁排查:五哲学家问题wg.Wait()偶现goroutine全部休眠错误

问题根因分析
  1. 互斥锁值传递问题
    sync.Mutex是值类型,原始代码中eat函数的left、right参数是值传递,每个协程拿到的是锁的副本,加锁操作完全不生效,没有互斥效果,所有哲学家可以同时拿到左右筷子,全部阻塞在等待host授权。你后续改成传引用解决了这个问题,但还有其他潜在问题。
  2. 随机数生成逻辑错误
    RandBool函数每次调用都执行rand.Seed(time.Now().UnixNano()),多个协程启动时间接近时,UnixNano时间戳相同,会导致所有协程生成的随机数一致,所有哲学家拿锁的顺序完全相同,大概率出现所有人同时拿到左锁、等待右锁的情况,直接触发死锁。
  3. 无缓冲通道的竞态阻塞
    permissionChannel是无缓冲通道,host发送授权时必须有哲学家刚好在接收才能发送成功。如果此时所有哲学家都在抢筷子锁,没有协程读通道,host的发送操作会阻塞,同时哲学家拿完锁后等不到授权,形成循环阻塞,触发死锁。你将通道改成带缓冲的之后,host可以提前把授权存入缓冲区,不需要阻塞等待接收,降低了竞态概率,所以问题暂时得到解决,但没有根除死锁风险。
  4. 执行顺序不合理
    原始代码的逻辑是先拿筷子锁→再等授权,拿着稀缺的筷子资源等待授权,会导致资源浪费,进一步提升死锁概率。
完整修复方案
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.14 05:42:03