在计算机科学中,哲学家就餐问题是一个经典的同步问题,它描述了五名哲学家围坐在一张圆桌旁,每人面前有一碗意大利面,左右各有一把叉子。哲学家们的生活状态只有两种:思考和就餐。他们只有在左右两边的叉子都可用时才能就餐。当他们就餐完毕后,就会放下叉子开始思考,此时叉子可供其他人使用。这个问题的挑战在于设计一个系统,使得所有哲学家都能持续地思考和就餐,而无需任何形式的通信,同时确保没有哲学家饿死,也不会发生死锁。
虽然场景中只提到了哲学家的两种状态:思考和就餐,但实际上存在第三种状态,即思考但寻找就餐机会,换句话说,哲学家饿了。因此,哲学家的活动可以被分解为三个连续的阶段:思考、饥饿和就餐。
任务基异步模式(Task-based Asynchronous Pattern)允许为每个哲学家执行一系列连续的任务,而不需要额外的编码。所需的只是创建一个哲学家类和一个叉子类。哲学家类需要有一个异步方法来实现哲学家经历的三个阶段,并使用await关键字来管理从一个阶段过渡到下一个阶段。叉子类只需要一个属性IsNotInUse来指示叉子的可用性。
思考阶段是最容易实现的。它持续一个随机的时间长度。计时是异步进行的,这允许其他哲学家改变他们的状态。它还允许更新桌子上的叉子的可用性。
while (true)
{
Mode = PhilosopherMode.Thinking;
int thinkingTime = random.Next(Constants.ThinkingMin, Constants.ThinkingMax);
int eatingTime = random.Next(Constants.EatingMin, Constants.EatingMax);
// 异步等待思考时间过去
await Task.Delay(TimeSpan.FromSeconds(thinkingTime), ct);
Mode = PhilosopherMode.Hungry;
...
}
这里的代码大多数是在单线程UI线程上运行的。不是的这部分是调用await Task.Delay(TimeSpan.FromSeconds(thinkingTime), ct)。这是一个异步等待。UI线程被释放,允许它运行其他实例的哲学家类发布的委托。延迟期过后,线程被回调到方法中,并运行继续部分,即await语句之后的部分。Task.Delay的ct参数是一个CancellationToken,它允许方法被取消。
饥饿阶段由一个循环组成,该循环异步等待一个短暂的时间间隔,然后确定两个所需的叉子是否可用。如果它们可用,循环将被打破,开始就餐阶段。
while (!isEating)
{
// 每几毫秒监控两个叉子是否在使用
await Task.Delay(Constants.MonitoringIntervalMilliSecs, ct);
// 如果叉子没有在使用,拿起它们开始就餐
if (LeftFork.IsNotInUse && RightFork.IsNotInUse)
{
LeftFork.IsNotInUse = false;
RightFork.IsNotInUse = false;
isEating = true;
}
}
Mode = PhilosopherMode.Eating;
就餐阶段很简单,就是等待一个随机的时间长度,然后放下叉子,停止就餐。
await Task.Delay(TimeSpan.FromSeconds(eatingTime), ct);
// 就餐时间过后,放下叉子,停止就餐
LeftFork.IsNotInUse = true;
RightFork.IsNotInUse = true;
isEating = false;
叉子是共享资源,所以它们的状态的任何变化都会反映在相邻哲学家的叉子上。在演示应用中,就餐和思考的时间范围设置得差不多,但实际上,就餐期会更短。除非哲学家是一个享乐主义者。
演示是一个WPF应用程序。每个哲学家由一个棋子图像表示。图像实际上是一个轮廓,它被放置在一个背景矩形上,这样矩形的颜色就形成了图像的背景颜色。在Xaml中,每个矩形的Fill Brush绑定到视图模型中的Philosopher.Mode属性,每个叉子图像的Visibility绑定到视图模型中的Fork.IsNotInUse属性。这种安排使得两种类型的图像都能改变它们的状态。
哲学家类有一个ModeChangedEvent和一个StartAsync方法。StartAsync方法通过在while循环中使用一系列定时的await来管理思考、饥饿和就餐的阶段。
public async Task StartAsync(Random random, CancellationToken ct)
{
bool isEating = false;
// 'while'循环在取消令牌取消Task.Delay时结束
while (true)
{
Mode = PhilosopherMode.Thinking;
int thinkingTime = random.Next(Constants.ThinkingMin, Constants.ThinkingMax);
int eatingTime = random.Next(Constants.EatingMin, Constants.EatingMax);
// 异步等待思考时间过去
await Task.Delay(TimeSpan.FromSeconds(thinkingTime), ct);
Mode = PhilosopherMode.Hungry;
while (!isEating)
{
// 每几毫秒监控两个叉子是否在使用
await Task.Delay(Constants.MonitoringIntervalMilliSecs, ct);
// 如果叉子没有在使用,拿起它们开始就餐
if (LeftFork.IsNotInUse && RightFork.IsNotInUse)
{
LeftFork.IsNotInUse = false;
RightFork.IsNotInUse = false;
isEating = true;
}
}
Mode = PhilosopherMode.Eating;
await Task.Delay(TimeSpan.FromSeconds(eatingTime), ct);
// 就餐时间过后,放下叉子,停止就餐
LeftFork.IsNotInUse = true;
RightFork.IsNotInUse = true;
isEating = false;
}
}
这个方法看起来可以重构。但是async关键字是一个编译器指令,它导致在幕后生成了很多代码。鉴于此,最好不要将async方法重构为一系列调用其他async方法的调用。
视图模型的大部分内容是定义每个哲学家的PhilosopherMode属性和每个叉子的IsNotInUse属性。模拟是通过调用OnStartAsync方法开始的。这个方法首先为所有哲学家调用async Task StartAsync(Random random, CancellationToken ct)方法,并将返回的Tasks存储在List中。然后异步等待所有Tasks完成。
private async void OnStartAsync(object arg)
{
IsStarted = true;
cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Random random = new Random();
List philosopherTasks = new List();
foreach (Philosopher philosopher in philosophers)
{
// 启动所有异步方法并存储返回的任务
philosopherTasks.Add(philosopher.StartAsync(random, token));
}
try
{
// 异步等待所有任务结束
// 取消令牌将取消它们全部
await Task.WhenAll(philosopherTasks);
}
catch (TaskCanceledException)
{
IsStarted = false;
InitialisePhilsMode();
InitialiseForks();
}
}
private void OnCancel(object arg)
{
cts.Cancel();
IsStarted = false;
}