用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](右) - 但第 4 位(索引 4)改为先拿
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大,但这里无法预知谁该被唤醒,只能全唤
容易被忽略的三个实际坑
很多示例跑起来看似正常,但一加压或换环境就出问题。真正上线要考虑这些:
- 没有设置线程名称或 ID 日志,导致并发行为无法追踪 —— 建议在
Console.WriteLine中带上Thread.CurrentThread.ManagedThreadId - 未处理
ThreadAbortException或中断,.NET 6+ 中Thread.Abort已废弃,必须依赖CancellationToken配合Monitor.TryEnter超时退出 - 所有 fork 对象都是静态的,但如果程序是 long-running service,要注意生命周期管理 —— 叉子对象本身无状态,但若未来扩展为带计数器或超时逻辑,就得考虑 Dispose 模式
哲学家问题不是为了造轮子,而是训练对锁顺序、条件竞争、唤醒丢失的直觉。代码越短,越要盯住那几行 Enter/Exit 的配对和位置。










