使用 Concurrency Explorer 解决“睡着的理发师”问题。





5.00/5 (1投票)
“睡着的理发师”问题是一个经典的进程间通信问题,与使用标准的并行或异步编码技术相比,使用 Concurrency Explorer 等工具进行研究和探索会更加容易。
引言
Concurrency Explorer (ConcX) 提供了探索和解决各种并行或异步问题的工具和功能,包括“睡着的理发师”问题,维基百科对其描述如下:
“在计算机科学中,“睡着的理发师”问题是操作系统之间多个进程之间一个经典的进程间通信和同步问题。该问题类似于如何在有顾客时让理发师工作,在没有顾客时休息,并以有序的方式进行。
该类比基于一家假设的理发店,只有一位理发师。理发师有一个理发椅在理发室,以及一个包含若干椅子的等候室。当理发师完成为顾客理发后,他会解散顾客并前往等候室查看是否有其他等待的顾客。如果有,他会带一位顾客到椅子旁理发。如果没有,他会回到椅子上睡觉。
每位顾客到来时,都会查看理发师在做什么。如果理发师在睡觉,顾客会叫醒他并坐在理发椅上。如果理发师正在理发,顾客会留在等候室。如果等候室有空椅子,顾客就会坐下并等待。如果等候室没有空椅子,顾客就会离开。”
维基百科文章随后讨论了各种并发问题,例如:
-
理发师和新来的顾客同时查看对方的状态,导致死锁,因为在那一刻
-
理发师看到椅子上没有人,认为等候室是空的,于是去睡觉
-
顾客认为理发师很忙,所以不尝试叫醒理发师,只是耐心等待理发师
-
-
两位顾客同时到达,看到理发师已经很忙,然后都试图坐同一把椅子,导致:
-
两位顾客都试图占用同一把椅子,或者
-
两位顾客都未能获得椅子,因此两位顾客都离开了
-
取决于进程间通信的定义是否完善,也可能发生其他并发问题。但别走神,开始在脑海中规划你的代码解决方案,先考虑使用 ConcX 来解决“睡着的理发师”问题。
解决“睡着的理发师”问题
Concurrency Explorer (ConcX) 是一个免费的开源工具,可从 Avian Computing Project 获取。ConcX 是一个交互式 GUI 工具,旨在帮助我们思考并行问题。ConcX 鼓励我们将并行问题/程序想象成一群鸟,每只鸟都独立地(吃、睡等)运作,但也会与其他鸟群竞争与合作。
Customer、Barber、ShopManager 和 Chair 对象都包含在 ConcX 的下载中。您可以逐个添加一个或多个这些对象,也可以直接加载标准 ConcX 下载中包含的 barbershop20cust.bird 文件。此文件将加载并配置一个理发师、一个店铺经理和二十个顾客。
对象加载完成后,ConcX 将显示如下截图:
左侧的编号选项卡代表已加载并配置为理发师和顾客角色的鸟。您可以单击各个选项卡以查看每个鸟的配置详细信息。屏幕中央是鸟的名称和类型列表,旁边是它们的个人进度条。每当一只鸟(理发师、顾客或店铺经理)成功“进食”时,其进度条就会增长。进度条是实时更新的,因此无需等到运行完成即可查看每个线程是否按预期工作。
单击屏幕左下角的“Start All”(全部开始)按钮。ConcX 屏幕右侧的活动进度条将开始增长,这与每个鸟成功进食的次数相关(参见下图)。
每个顾客的进度条仅在他们实际等待时才会增长。每个顾客的到达时间都已预先配置,因此他们不会同时到达。当顾客找不到椅子时,他们就会放弃并离开。如果顾客等待时间过长(单独配置),他们就会放弃并离开。成功理发的顾客(或放弃的顾客)将终止其线程,其进度条将停止增长。
每次理发师理发时,他的进度条都会增长;如果只有一个顾客,理发师的进度条只会增长一次。只有店铺经理的进度条在整个运行过程中会持续增长,因为他会不断检查发型报告的更新。当他的进度条停止增长时,单击屏幕右上角的 TupleTree 选项卡,查看运行结果。本文稍后将详细介绍 TupleTree。
“睡着的理发师”结果
下图显示了运行标准的“睡着的理发师”解决方案后 TupleTree 的内容。DailyTransactionPod 包含由 ShopManager 生成并持续更新的报告。
报告包括:
-
接受理发的顾客姓名以及为他们理发的理发师姓名
-
未能找到椅子或耐心耗尽而离开的顾客姓名。两者都代表了错失的机会。
-
按理发师列出的理发列表
-
被叫但缺席的顾客列表
请注意,顾客均按字母顺序排列。这是因为顾客 #1 至 #20 都按字母顺序命名并按此顺序到达。这使得我们可以轻松确定顾客是否确实按到达顺序获得理发。
这是从 TupleTree 屏幕复制的完整报告:
Customers Completed Report CustName Arrvd At Start At FinishAt $Paid BarberName Adambb9 17:001 17:060 18:060 $10.00 Zeb Bertc4a 17:919 18:075 20:075 $20.00 Zeb Chada24 18:937 20:098 23:099 $30.00 Zeb Dave9e1 20:954 23:133 24:633 $15.00 Zeb Evanb05 22:972 24:662 27:163 $25.00 Zeb Greg14f 27:007 27:549 29:550 $20.00 Zeb Hanka66 27:026 29:605 32:606 $30.00 Zeb Ivan42a 27:045 32:631 34:132 $15.00 Zeb Jake3da 31:060 34:167 36:668 $25.00 Zeb Kentbd3 33:077 36:679 37:680 $10.00 Zeb Lukebe8 34:096 37:746 39:747 $20.00 Zeb Mark2f9 36:113 39:756 42:756 $30.00 Zeb Natef97 37:131 42:764 44:265 $15.00 Zeb Otto7d3 39:149 44:302 46:803 $25.00 Zeb Todde18 42:233 48:249 50:750 $25.00 Zeb ----------------------------------------------- Customers Who Gave Up or Couldn't Find a Chair CustName Arrvd At Gave Up At Fredc86 23:089 26:100 Petec74 39:170 39:170 Quin77b 39:189 39:189 Rami325 39:207 39:207 Stan621 39:226 39:226 ----------------------------------------------- Barber Productivity Report Barber Name Haircut Amt Haircut Time Zeb $10.0 10 minutes Zeb $20.0 20 minutes Zeb $30.0 30 minutes Zeb $15.0 15 minutes Zeb $25.0 25 minutes Zeb $20.0 20 minutes Zeb $30.0 30 minutes Zeb $15.0 15 minutes Zeb $25.0 25 minutes Zeb $10.0 10 minutes Zeb $20.0 20 minutes Zeb $30.0 30 minutes Zeb $15.0 15 minutes Zeb $25.0 25 minutes Zeb $25.0 25 minutes ----------------------------------------------- Barber Called Absent Customer Report Barber Name Cust Name Called At Zeb Fredc86 27:534 Zeb Petec74 47:166 Zeb Quin77b 47:482 Zeb Rami325 47:841 Zeb Stan621 48:202 -----------------------------------------------
请注意,有 5 位顾客放弃等待(1 位等待时间过长,4 位找不到椅子)。报告显示 Fred 等待了大约 3 秒后放弃,这与他 3 秒的耐心设置相符。其他 4 位顾客的到达时间和离开时间几乎相同,这表明他们都因为找不到椅子而离开。由于 Otto、Pete、Quin、Rami 和 Stan 的到达时间相差不到 80 毫秒,这表明他们都是同时到达的,但只有 Otto 找到了椅子并接受了理发。
让我们看看做一些小小的改变会如何影响结果。
探索“睡着的理发师”的可能性
下图显示已选择了选项卡 #1(Mr Smith,店铺经理),并且也选择了他的 Externally Managed Variable (XMV) 选项卡。“Chairs in Waiting Room”(等候室椅子)字段为空,这意味着 ShopManager 放置了默认数量的椅子(3 把)。通过将 XMV 字段更新为 5,我们可以重新运行模拟,看看增加椅子数量是否能解决问题。无需重新编译。
单击右下角的“Clear All Activities”(清除所有活动)按钮,然后单击屏幕左下角的“Start All”(全部开始)按钮。
以下报告节选显示了放弃等待的顾客:
Customers Who Gave Up or Couldn't Find a Chair CustName Arrvd At Gave Up At Fredbd3 39:798 42:904 Rami226 55:812 55:812 Stan7f5 55:819 55:819 -----------------------------------------------
<meta charset="utf-8" />
现在只有三位顾客放弃了:Impatient Fred 和那五位同时进来的顾客中的最后两位。如果我们增加几把椅子,清除结果然后重新运行模拟,报告的“Customer Gave Up”(顾客放弃)部分将只包含 Impatient Fred。
但是,如果理发店很小,放不下 7 把椅子呢?为了仅在 5 位或更多顾客同时到达的罕见情况下扩大店铺,成本将相当高。让我们看看如果我们增加第二位理发师而不是增加椅子会发生什么。
首先,单击选项卡 #1,将 Mr Smith 的“Chairs in Waiting Room”值留空/归零。
接下来,单击“Add New Bird”(添加新鸟)按钮(屏幕底部中间),并添加一个新的 Barber 鸟,如下所示:
单击“Add New Bird”按钮,然后:
-
(鸟)类型:在 Type 字段中输入“bar”,然后按 Tab 键。ConcX 将找到多个以“bar”开头的鸟类类型,并弹出显示所有匹配项的 ComboBox。如果尚未突出显示,请选择 Barber.class,然后单击 OK 按钮。
-
进食:在 Food Type 字段中输入“bar”,然后按 Tab 键。ConcX 将找到多个以“bar”开头的食物豆,并弹出显示所有匹配项的 ComboBox。如果尚未突出显示,请选择 BarberBacklogPod,然后单击 OK 按钮。
-
储存食物:在 Food Type 字段中输入“null”,然后按 Tab 键。ConcX 将只找到一个匹配的食物豆,并填充 NullPod 的完整名称。
-
给理发师起一个恰当的随意名字,例如 Yank 或 Willy。
-
单击理发师的“Vitality”(活力)选项卡,将其“Stamina”(耐力)更改为负数,例如 -5000。负数可以防止鸟在长时间不进食的情况下死亡。
单击“Clear All Activities”(清除所有活动)按钮,然后单击“Start All”(全部开始)按钮。
运行完成后,店铺经理的结果将与以下类似:
Customers Completed Report CustName Arrvd At Start At FinishAt $Paid BarberName Adam81e 57:134 57:136 58:136 $10.00 Zeb Bertfaa 58:050 58:084 00:085 $20.00 Yank Chad422 59:067 59:079 02:079 $30.00 Zeb Dave5a8 01:085 01:121 02:620 $15.00 Yank Evan447 03:102 03:108 05:608 $25.00 Zeb Freda5f 05:118 05:137 06:137 $10.00 Yank Greg4a4 07:135 07:142 09:142 $20.00 Zeb Hankac2 07:153 07:191 10:191 $30.00 Yank Ivan8fe 07:171 09:171 10:672 $15.00 Zeb Jake753 11:185 11:240 13:740 $25.00 Yank Kent3a6 13:203 13:235 14:235 $10.00 Zeb Luke0cb 14:219 14:233 16:233 $20.00 Yank Nate4cf 17:253 17:275 18:775 $15.00 Zeb Mark0b8 16:236 16:252 19:253 $30.00 Yank Petedb7 19:290 19:321 20:321 $10.00 Yank Otto736 19:272 19:299 21:798 $25.00 Zeb Quin94d 19:309 20:330 22:330 $20.00 Yank Toddee5 22:354 22:371 24:870 $25.00 Yank ----------------------------------------------- Customers Who Gave Up or Couldn't Find a Chair CustName Arrvd At Gave Up At Ramif1e 19:327 19:327 Standec 19:346 19:346 ----------------------------------------------- Barber Productivity Report Barber Name Haircut Amt Haircut Time Zeb $10.0 10 minutes Yank $20.0 20 minutes Zeb $30.0 30 minutes Yank $15.0 15 minutes Zeb $25.0 25 minutes Yank $10.0 10 minutes Zeb $20.0 20 minutes Yank $30.0 30 minutes Zeb $15.0 15 minutes Yank $25.0 25 minutes Zeb $10.0 10 minutes Yank $20.0 20 minutes Zeb $15.0 15 minutes Yank $30.0 30 minutes Yank $10.0 10 minutes Zeb $25.0 25 minutes Yank $20.0 20 minutes Yank $25.0 25 minutes ----------------------------------------------- Barber Called Absent Customer Barber Name Cust Name Called At Zeb Ramif1e 22:124 Zeb Standec 22:457 -----------------------------------------------
<meta charset="utf-8" />
关于这些结果有几点需要注意:
-
有了两位理发师,Impatient Fred 得到了理发,因为他不必等待太久。
-
只有两位顾客因为找不到椅子而放弃。
-
“Completed Customers”(完成的顾客)报告按理发完成顺序而非到达顺序列出顾客。这表明理发师们在协作但异步工作;一位理发师不必等待另一位理发师完成,而是始终去处理等待时间最长的顾客。
-
店铺经理知道是哪位理发师进行了哪些理发,而无需重新编程。
让我们增加第三位理发师,看看会发生什么。单击“Add New Bird”(添加新鸟)按钮,然后按照上述说明添加另一位 Barber 鸟。现在单击“Clear All Activities”(清除所有活动),然后单击“Start All”(全部开始)。
重新运行模拟后,我们得到以下结果:
Customers Who Gave Up or Couldn't Find a Chair CustName Arrvd At Gave Up At Ramibea 11:000 11:000 Stanf61 11:018 11:018 -----------------------------------------------
<meta charset="utf-8" />这个结果表明增加第三位理发师并没有帮助;仍然有两位顾客找不到椅子。所以,让我们解雇第三位理发师(通过将其类型更改为 Not Found 来禁用他的线程),并将等候室的椅子数量更改为 4。现在,当我们清除所有活动并启动所有活动时,20 位顾客中的每一位都能获得理发。
这意味着我们已经确定了两种不同的方式来服务每一位顾客:
- 通过将等候室椅子数量增加到 7 把
- 通过增加一位理发师,并将等候室椅子数量增加到 4 把
<meta charset="utf-8" />
ConcX 概念
让我们看一下 ConcX 的基本概念。利用 Avian Computing 的思想,ConcX 将各种线程状态映射到鸟类的自然行为,如孵化(thread.start)、打盹(thread.sleep)、死亡(thread.stop)等,以便更容易地思考和描述每个线程的动作。每只鸟都遵循一个标准的生命周期;一旦孵化,它们就会寻找食物,如果找到特定种类的食物就会消化,储存任何产生的食物(就像有些鸟会藏起剩饭),然后小睡。当鸟类年龄太大或吃不饱时,它们就会死亡。以自然术语表达线程的动作是为了使其更轻松、更自然地思考线程。
<meta charset="utf-8" />
ConcX 中的每个“鸟”(线程)都消耗一种食物豆,并在“TupleTree”中储存一种食物豆。“TupleTree”是 ConcX 中 元组空间 的简化版本,这是最初为 Linda 协调语言 开发的一个概念。元组空间 是用作元组数据存储库的共享关联内存。Linda 最初于 1986 年由 Sudhir Ahuja、David Gelernter、Nicholas Carriero 等人创建。Linda 是许多主要产品的基础,包括 Sun 的 JavaSpaces、IBM 的 TSpaces 以及各种语言中的许多其他产品。
在 ConcX 中,TupleTree 是一个单例,它使用同步方法与所有鸟类共享数据。TupleTree 中的任何对象都可以被任何知道如何请求该类型对象的鸟类检索。由于 TupleTree 使用同步方法管理其对象,因此用户无需创建或编写互斥锁、锁或其他用于并发对象共享对象的机制。相反,TupleTree 管理所有数据共享,因此您可以专注于需要对数据进行的操作。
传统的 wait 和 notify 并发工具就像一个传统的电话系统;如果你拨打的电话对方在线,那效果非常好。不幸的是,如果双方不能同时在线,就什么都做不了。
Linda 更像一个电子邮件系统,发送者和接收者会方便地检查和回复电子邮件,而不是相互打断。在 ConcX 中,TupleTree 就像一个电子邮件服务器,但它存储着充当共享资源的食物豆。通过这种方式,Linda 为多个异步线程之间的数据共享提供了一种安全、简单且健壮的机制。
ConcX 中的运行时信息
ConcX 最初的目标之一是提供对其运行时上下文的即时访问,了解每个线程在运行时发生的情况。为此,ConcX 提供的信息比实时更新进度条更多。ConcX 以多种方式自动记录每次运行,包括:
-
TupleTree 中的每个食物豆都有一个完整的带时间戳的历史记录,记录了该食物豆发生的所有事件以及是哪个鸟导致了该事件。
-
每只鸟都维护自己的带时间戳的事件历史记录,例如何时寻找食物、找到了什么食物、何时消化并储存了食物等等。
以下节选将有助于说明如何使用记录的信息。首先,从运行结束时留在 TupleTree 中的 Chair-2 食物豆,我们可以看到 Bert 何时坐在椅子上(该次运行中坐在 Chair-2 上的所有顾客也都有记录)。
11:14:23:853,WaitingRoomChairPod,Bert78b found a chair in waiting room 11:14:24:127,WaitingRoomChairPod,Bert78b gave up chair to get haircut
<meta charset="utf-8" />
接下来,这是来自 Customer #2,Bert 的个人历史记录节选,可在其 History(历史记录)选项卡上找到:
11:14:23:853,Bert,Bert78b found a chair in waiting room 11:14:23:853,Bert,Created CustInfoFindByNamePod,Bert78b 11:14:23:853,Bert,Created BarberBacklogPod,Bert78b 11:14:23:853,Bert,Looking for food, 11:14:23:853,Bert,eating foodType,Bert78b 11:14:23:853,Bert,Napping,107ms 11:14:23:961,Bert,Looking for food, 11:14:23:961,Bert,eating foodType,Bert78b 11:14:23:961,Bert,Napping,166ms 11:14:24:127,Bert,Looking for food, 11:14:24:127,Bert,eating foodType,Bert78b 11:14:24:127,Bert,bird lifetime was set to 40300 millis 11:14:24:127,Bert,haircut in process
<meta charset="utf-8" />
Bert 的历史记录显示,他在 23:853 时找到了一把空椅子(Chair #2)。等待了大约 250 毫秒(寻找食物和打盹)后,Bert 在 24:127 时记录了他的理发正在进行中。
为了交叉验证信息,理发师(Zeb)的历史记录显示他在 23:963 时在睡觉,然后从等待列表(BarberBacklogPod)中获取了下一个名字,然后叫了他(Bert)。在 Bert 小睡 166 毫秒(从 23:995 到 24:127)的短暂延迟后,期望的理发(2000 毫秒)就完成了(从 23:995 到 25:996)。
11:14:23:963,Zeb,Napping,32ms 11:14:23:995,Zeb,Looking for food, 11:14:23:995,Zeb,Got 1 pods from TupleTree 11:14:23:995,Zeb,eating foodType,barbershop\BarberBacklogPod,Zeb 11:14:23:995,Zeb,Called Bert78b for haircut 11:14:23:995,Zeb,BEFORE Haircut retrieved Bert78b info on attempt 0 11:14:25:996,Zeb,AFTER Haircut retrieved Bert78b info on attempt 0 11:14:25:996,Zeb,ATE_FOOD1-->strFood1 NullPod,STORE_ONE by,barbershop\Barber 11:14:25:996,Zeb,Napping,49ms
虽然上面显示的节选仅确认了理发店按预期运行,但当出现问题时,食物豆和鸟类的组合历史记录最有价值。带时间戳的历史记录条目使得在遇到错误时,能够重建运行时上下文,直至每个线程的状态。
<meta charset="utf-8" />
“睡着的理发师”结论
ConcX 解决“睡着的理发师”问题的方案比维基百科上描述的原始问题复杂得多,它允许有多个理发师、近乎实时报告的店铺经理、不耐烦的顾客等等。与大多数其他解决方案相比,它更容易运行和修改,同时提供了更好的保证,证明它完成了预期的工作。ConcX 解决方案在扩展时也非常健壮;下面的截图显示了当 barbershop flock 文件加载五次并运行时会发生什么。所有 5 位理发师、所有 5 位店铺经理和所有 100 位顾客都按预期工作。
本文展示了 ConcX 在探索并发程序解决方案方面的一些优势,包括:
-
用于配置和运行多个并发线程的 GUI 环境。
-
根据需要添加新线程。
-
在运行期间随时启动和停止单个线程或所有线程。
-
简单地修改单个线程设置,以帮助识别哪些设置是关键的,哪些是不重要的。
-
GUI 屏幕上每个线程进度的实时更新,为用户提供即时视觉反馈。
-
共享数据对象的带时间戳事件历史记录,包括是哪个鸟(线程)拥有该数据对象。
-
每次运行的每个鸟(线程)的带时间戳事件历史记录,并为每个鸟选择不同的详细程度。
ConcX 的设计旨在支持在健壮的高反馈环境中快速轻松地进行修改,以促进并发程序的探索。ConcX 是一个免费的开源工具,旨在促进对并发程序的更高层次的思考;ConcX 让我们能够思考我们希望线程**做什么**,而不是思考**如何**编写我们希望它们做的事情。
历史
2018 年 7 月 27 日:文档首次发布