用Monitor实现哲学家进餐问题的核心是打破循环等待:第4位哲学家反向取叉(先右后左),配合try/finally确保双锁安全释放、CancellationToken控制退出、Thread.Sleep模拟行为,并避免lock无法嵌套加锁的缺陷。
Monitor 实现最简可运行的哲学家进餐核心是避免死锁:五个哲学家不能同时拿起左边叉子。最稳妥的做法是让其中一个哲学家「反向拿叉」——先右后左,打破循环等待条件。
关键点:Monitor.Enter 和 Monitor.Exit 必须成对出现,且必须用 try/finally 保证释放;所有叉子对象要预先创建并共享;哲学家线程需有明确退出机制(如 CancellationToken)。
object 实例表示,放在数组里:private static readonly object[] forks = Enumerable.Range(0, 5).Select(_ => new object()).ToArray();
i 位哲学家默认先拿 forks[i](左),再拿 forks[(i + 1) % 5](右)forks[(i + 1) % 5](右),再拿 forks[i](左),打破对称性Thread.Sleep 模拟思考/进食时间,否则会跑得太快看不出竞争效果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 无法嵌套锁定两个不同对象而不留隐患。
lock(forks[left]) { lock(forks[right]) { ... } },在第二个 lock 失败时,第一个锁不会自动释放Monitor.TryEnter 可设超时,适合做“尝试拿叉失败就放弃”策略(避免饥饿),而 lock 会一直阻塞Monitor.TryEnter(fork, timeout) 返回值,失败就 Thread.Sleep 后重试Wait / Pulse 版本:更贴近原始问题语义原始哲学家问题强调「只有左右叉都可用时才开始吃」,而不是强行抢锁。这时该用 Monitor.Wait 让线程等待条件成立,用 Monitor.PulseAll 唤醒所有等待者。
你需要为每把叉子维护一个「是否空闲」状态,并用一个全局锁保护状态检查。哲学家进入「想吃」状态后,轮询检查两把叉子是否都空闲;若不满足,Monitor.Wait 挂起自己;一旦某人吃完放下叉子,就 Monitor.PulseAll 唤醒所有人重新判断。
bool[] 或类似结构,且读写必须被同一把锁保护Wait 前必须在 while 循环里检查条件,防止虚假唤醒PulseAll 开销比 Pulse 大,但这里无法预知谁该被唤醒,只能全唤很多示例跑起来看似正常,但一加压或换环境就出问题。真正上线要考虑这些:
Console.WriteLine 中带上 Thread.CurrentThread.ManagedThreadId
ThreadAbortException 或中断,.NET 6+ 中 Thread.Abort 已废弃,必须依
赖 CancellationToken 配合 Monitor.TryEnter 超时退出哲学家问题不是为了造轮子,而是训练对锁顺序、条件竞争、唤醒丢失的直觉。代码越短,越要盯住那几行 Enter/Exit 的配对和位置。