哲学家就餐问题的异步解决方案

在计算机科学中,哲学家就餐问题是一个经典的同步问题,它描述了五名哲学家围坐在一张圆桌旁,每人面前有一碗意大利面,左右各有一把叉子。哲学家们的生活状态只有两种:思考和就餐。他们只有在左右两边的叉子都可用时才能就餐。当他们就餐完毕后,就会放下叉子开始思考,此时叉子可供其他人使用。这个问题的挑战在于设计一个系统,使得所有哲学家都能持续地思考和就餐,而无需任何形式的通信,同时确保没有哲学家饿死,也不会发生死锁。

问题分析

虽然场景中只提到了哲学家的两种状态:思考和就餐,但实际上存在第三种状态,即思考但寻找就餐机会,换句话说,哲学家饿了。因此,哲学家的活动可以被分解为三个连续的阶段:思考、饥饿和就餐。

解决方案

任务基异步模式(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; }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485