贝利信息

c# 如何用c#实现哲学家进餐问题 Dining Philosophers Problem

日期:2026-01-03 00:00 / 作者:煙雲
用Monitor实现哲学家进餐问题的核心是打破循环等待:第4位哲学家反向取叉(先右后左),配合try/finally确保双锁安全释放、CancellationToken控制退出、Thread.Sleep模拟行为,并避免lock无法嵌套加锁的缺陷。

Monitor 实现最简可运行的哲学家进餐

核心是避免死锁:五个哲学家不能同时拿起左边叉子。最稳妥的做法是让其中一个哲学家「反向拿叉」——先右后左,打破循环等待条件。

关键点:Monitor.EnterMonitor.Exit 必须成对出现,且必须用 try/finally 保证释放;所有叉子对象要预先创建并共享;哲学家线程需有明确退出机制(如 CancellationToken)。

using System;
using System.Threading;

class Program
{
    private static readonly object[] forks = Enumerable.Range(0, 5).Select(_ => new object()).ToArray();
    private static readonly CancellationTokenSource cts = new();

    static void Main()
    {
        var philosophers = Enumerable.Range(0, 5)
            .Select(i => new Thread(() => Philosopher(i)))
            .ToArray();

        foreach (var t in philosophers) t.Start();
        Thread.Sleep(5000);
        cts.Cancel();
        foreach (var t in philosophers) t.Join();
    }

    static void Philosopher(int id)
    {
        while (!cts.Token.IsCancellationRequested)
        {
            Console.WriteLine($"Philosopher {id} is thinking...");
            Thread.Sleep(100);

            // 左右叉子索引
            int left = id;
            int right = (id + 1) % 5;

            // 最后一位哲学家反向取叉,避免死锁
            if (id == 4)
            {
                Monitor.Enter(forks[right]);
                Monitor.Enter(forks[left]);
            }
            else
            {
                Monitor.Enter(forks[left]);
                Monitor.Enter(forks[right]);
            }

            try
            {
                Console.WriteLine($"Philosopher {id} is eating...");
                Thread.Sleep(200);
            }
            finally
            {
                Monitor.Exit(forks[left]);
                Monitor.Exit(forks[right]);
            }
        }
    }
}

为什么不用 lock 语句而用 Monitor

lock(obj) 底层就是 Monitor.Enter/Exit,但它只支持单个对象加锁。哲学家要同时持有两个叉子,必须显式控制两把锁的获取顺序和异常安全释放 —— lock 无法嵌套锁定两个不同对象而不留隐患。

Wait / Pulse 版本:更贴近原始问题语义

原始哲学家问题强调「只有左右叉都可用时才开始吃」,而不是强行抢锁。这时该用 Monitor.Wait 让线程等待条件成立,用 Monitor.PulseAll 唤醒所有等待者。

你需要为每把叉子维护一个「是否空闲」状态,并用一个全局锁保护状态检查。哲学家进入「想吃」状态后,轮询检查两把叉子是否都空闲;若不满足,Monitor.Wait 挂起自己;一旦某人吃完放下叉子,就 Monitor.PulseAll 唤醒所有人重新判断。

容易被忽略的三个实际坑

很多示例跑起来看似正常,但一加压或换环境就出问题。真正上线要考虑这些:

哲学家问题不是为了造轮子,而是训练对锁顺序、条件竞争、唤醒丢失的直觉。代码越短,越要盯住那几行 Enter/Exit 的配对和位置。