
「BUAA OO」Unit-2 电梯调度

电梯
一、前言
考虑到第一次博客写的实在是太复杂,第六次作业的博客我希望能够按照更加清晰的结构来叙述我的设计。在这次博客中,我将解题思路单独叙述,再进一步叙述我的设计架构。
二、题目背景
重述题目,熟悉题目的话可以跳过这一部分。
第五次作业
1. 题目背景
- 六部电梯
- 每部电梯单独工作
- 每个人只能等它指定的一部电梯
- 每部电梯最多承载六个人
- 每部电梯在一楼和十一楼往返运行
- 根据题意,每部电梯需要在每一层楼都能停留,即不能出现高低层电梯和奇偶层电梯这类电梯
- 每次上下一层楼花费0.4秒
- 开关门各0.2秒,从门开始打开到完全关闭期间,乘客可以随意进出
- 电梯没有加速度,纸片人不会被门夹
- 电梯就是神,只要愿意就可以随意吃人吐人
- 人可能在任意时间在任意楼层按电梯(具体什么时候在哪层楼取决于输入)
- 输入不能使得初始楼层和目标楼层相等
2. 评价标准
课程组提供了两个标准。一个是正确性评判标准,一个是性能评判标准
正确性评判
- 每个人最后都必须到达目标楼层。
- 电梯最多搭载6人。
- 电梯不能飞速运行,比如电梯不能在小于0.4秒的时间内移动一层楼。
- 电梯总运行时间不能比课程组的标准运行时间(
)长太久。具体来说,不能比 长(单位:秒) - 关门后才能开门,开门后才能关门。
- 符合客观事实,比如不能开着门上下移动,不能让人不乘电梯瞬移到目标楼层之类的。具体要求见指导书。
性能评判
课程组提供了较为复杂的评估标准,不太容易懂。简单来说就是满足以下几点要求:
- 不要让系统运行影响电梯运行时间。(系统要快)
- 不要让一个人等太久。(等待时间只按电梯到抵达目标楼层的时间)(乘客要爽)
- 不要让电梯无效运行。(电费要少)
第六次作业
1. 题目背景
只叙述有改动的地方:
- 每个人不再制定电梯。
- 每个人可以被调度器分配到任何一个电梯,分配后除非重置,否则不可更改。
- 电梯在把分配到自己的乘客全部运送完之后,除非有新的乘客再被分配进来,否则不应该再移动(移动会判错)。
- 乘客在被分配到电梯后应该输出
RECEIVE
语句,也就是要输出一些东西表明乘客分配到了这部电梯。 - 电梯支持
RESET
命令,RESET
通过标准输入输入进来,电梯必须在5s内响应RESET
请求,并且在这5s中最多移动两层楼(输出两次ARRIVE
)。响应RESET
请求的标志为输出RESET_BEGIN
语句。 RESET
需要1.2s完成,并在完成时输出RESET_END
语句表明RESET
完成。RESET
是用于设置电梯的新的运行状态的,这包括移动一层楼的速度和满载人数。RESET
期间电梯里不能有人(可能需要踢人),RESET
要在关门时才能RESET
。RESET
期间电梯不能接受新乘客。- 先前被电梯接到的人在该电梯
RESET
后需要重新RECEIVE
,当然这也意味着电梯RESET
后可以原本的乘客可以不再坐这部电梯。
2. 评价标准
和上次作业几乎完全相同。
第七次作业
1. 题目背景
只叙述有改动的地方:
- 增加双轿厢电梯。电梯初始默认单轿厢电梯,接受第二类
RESET
后转变为双轿厢电梯。 - 增加第二类
RESET
请求,用于将电梯重置为双轿厢电梯,RESET
用时1.2s。相当于把单轿厢拆掉新建了一个双轿厢电梯。第二类RESET
请求包括双轿厢电梯的换乘楼层、移动速度、最大人数等。 - 双轿厢在没有收到
RECEIVE
请求时仍然可以移动。 - 双轿厢分为下层轿厢
轿厢A
和上层轿厢轿厢B
,轿厢A
只能在换乘楼层及其下方楼层移动,轿厢B
只能在换乘楼层及其上方楼层移动。 - 电梯在重置为双轿厢电梯后,
轿厢A
默认在换乘楼层的下面一层,轿厢B
默认在换乘楼层的上面一层, - 双轿厢电梯不接受
RESET
。输入保证不会出现这种情况。 - 显然,双轿厢电梯不能Crush。换乘楼层同时只能有一部电梯。
- 乘客只要离开轿厢(输出
OUT
)就需要重新RECEIVE
。
2. 评价标准
- 双轿厢电梯的耗电量是正常电梯的1/4。这无疑增加了影子电梯的编写难度。
三、解题思路
具体的代码设计架构在第四部分。不想看解题思路的可以跳过。
第五次作业
1. 我们有哪些需求?
纵观题目,我们可以得出一个简单的结构设计:
- 对于人来说:输入→分配到电梯→等电梯→搭电梯→出来
- 对于电梯来说:读等待的人→决定要不要接他→接/不接→送客
所以基本上,我们需要有这么几个类:
InputThread
输入线程 - 采购Schedule
分配线程 - 把人分给电梯吃Elevator
电梯线程 - 吃人吐人Client
顾客类 - 人
然后,我们接着模块化,简化对象但是增加对象数量:RequestQueue
请求队列 - 采购但是没有被分配的人WaitQueue
等待队列 - 分配但是没有被电梯吃掉的人Strategies
策略类 - 决定电梯先吃哪个人
剩余的类不起到决定性效果,只是起了一点辅助作用,在此不在列举。
2. 我们需要哪些线程?
先理一理要求和思路:
- 首先毫无疑问,我们需要1个输入线程。
- 六部电梯需要单独运行,因此需要一个6个电梯运行线程。
- 但是考虑到之后的作业,我们应该需要一个调度器,用于将乘客分配给不同的电梯。因此我们可以再新建1个调度器线程。
- 我们希望电梯开关门期间可以进入新乘客,而不是
sleep(200)
休眠200毫秒。考虑到休眠期间电梯是不会进行输入的,我们可以再新建6个电梯管理线程。 - 经过实践,我发现第四点思考难以实现也并不必要。直接删掉。
- 总结一下,1个输入,1个调度,6个电梯。
3. 线程与线程之间的关系如何(生产-消费模式)?
这是一个很奇怪的问题,在吃了苦头之前我甚至都没有问过自己这个问题。
电梯单元采用的多线程设计模式比较简单,即生产-消费模式。在我的设计中,生产消费关系有俩:输入和调度、调度和电梯。
(1) 输入线程和调度线程
输入进来的乘客需要交给调度线程,由调度线程经过合适的调度策略(在这次作业中,调度策略就是把乘客调度到它指定的电梯)交给电梯。因此输入和调度线程构成生产-消费关系:输入不断生产客户,调度器不断消费客户。而存放生产出来的产品的地方正是RequestQueue
类。
(2) 调度线程和电梯线程
调度器会将自己得到的客户交给电梯,由电梯进行消费。从输入进来的顾客会在输出后被释放,因此到这里为止搭乘电梯的顾客构成的供应链就到达链的末尾了。存放生产出来的客户的是我们的WaitQueue
类,也就是在指定电梯面前等待的客户构成的集合。这样的集合一共有六个,每个电梯各一个。
4. 什么是锁?什么是同步方法?
写这个板块纯粹是因为我在synchronized
方法上吃了苦头。总结起来我最开始不理解的地方大概就只有这么几点:
- 每一个
synchronized
部分一次只能由一个获得了指定锁的线程运行。这种指定的锁只有一把,当这个线程在运行的时候它就占有了锁,其他线程必定就没有占有锁,因此其他线程必定无法运行。 - 每一个
synchronized
锁就是一个对象。同步方法的锁就是这个同步方法所处的对象本身。静态同步方法锁的是类对象本身,简单理解就是这整个类的所有静态同步方法共同竞争的一把锁,它不会影响非静态同步方法。 - 没事别吃饱了撑的把所有方法都锁上。 只有线程与线程产生竞争的部分需要上锁。竞争的部分是设计决定的,适当地将方法设置为private方法或许是一个好的选择。
- 原则上同步方法都应该在交出锁之前
notify
,但是有一个情况例外:就是这个同步方法是被另一个同步方法调用的。这时这个同步方法哪怕结束了,锁也不会被释放。这时不需要notify
,否则就轮询了。 - 同一对象的同步方法调用该对象的同步方法是不会死锁的。 运行父方法就会得到这个对象的锁,而在调用叶方法的时候父方法会发现已经有锁了,于是大胆进入叶方法里头。
- 在生产-消费模型中,基本上只有涉及到共享数据对象的时候才会出现同步方法。 其他时候不怎么使用也不需要过多担心,否则可能就要狠狠担心一下了。
5. 电梯运行策略
详细见第五部分“五、调度策略选择”
第六次作业
1. 有哪些改动?
第六次作业的主要改动有两个方面:一个是RESET
,一个是乘客不知道进哪个电梯,需要调度器去调度乘客。
我们来分析一下我们现在的线程情况。
首先,一个输入线程,一个调度器线程,六个电梯线程,这八个线程应该是没得跑了。六个电梯显示关系实在是太复杂了,我们先考虑一个电梯,也就是先考虑3个线程先。(直觉告诉我们,六个电梯彼此应该没有太多的关系。)
graph TD in(输入) sch(调度器) ele(电梯)
然后检查一下这三个线程的关系:
graph LR in(输入) sch(调度器) ele(电梯) wq(等待队列) rq(请求/输入队列) rs(重置信号队列) in --> rq --> sch in --> rs --> ele sch --> wq --> ele ele --> rq
其中,箭头表示生产-消费传递链。然后可以分离出电梯这个模块:
1 | +------------------+ |
2. 线程终止条件
和第五次作业不同,第六次作业中电梯不再只是消费者:被电梯踢出来的乘客会重新进入调度器接受调度,因此电梯在某种意义上也是生产者。
生产和消费构成的闭环会导致一个问题:电梯和调度器线程根本停不下来! 调度器需要查看RequestQueue
判断是否停止:如果RequestQueue
为空并且结束了就把电梯也停下来。RequestQueue
的结束是依赖于电梯线程的,电梯线程只要不停,RequestQueue
就停不下来,调度器也停不下来,进而电梯也停不下来,使得程序无法结束运行。
为什么会出现这种情况呢?因为在闭环中只查看线程之间存放乘客的共享对象,是无法给生产消费链画上终止符号的。我们需要更多信息。对于调度器线程来说,它不仅仅需要知道共享对象的状态,还需要知道电梯线程的状态才行。再调度器得知输入线程结束了之后,调度器线程还需要确认电梯将所有的乘客都送完了才行。
所以解决方案很简单:调度器如果发现输入线程结束了,并且没有人需要调度,并且每个电梯都没有人了,就让所有电梯都停下来,并且自己结束运行。
3. 调度器调度策略
详细见第五部分“五、调度策略选择”
第七次作业
1. 有哪些改动
第七次作业是最后一次重构的机会了!我把握住了,然后写了一tuo…
个人认为第七次作业可能会遇到这些问题:
- 双轿厢电梯和单轿厢电梯一共有三个线程,但是不会同时启用。那么应该由谁来管理这些线程?
- 如何处理双轿厢电梯在换乘楼层的冲突?
- 设计单双轿厢电梯,线程之间如何交互?
2. 谁来管理电梯线程
第七次作业中的电梯线程的管理其实很简单:创建单轿厢电梯、销毁单轿厢电梯、创建双轿厢电梯。所以主要矛盾集中在单双轿厢电梯的切换上。很难让单轿厢电梯创建双轿厢电梯(因为逻辑上很奇怪)所以强烈建议加入一个中间层来管理所有电梯线程 (比如H Boy
提出的创建电梯井 ) 。线程的创建和销毁都是调用中间层的方法来完成的。
吴老师在第八次课提到了线程池,但是我没有使用过,并不清楚。
3. 如何处理双轿厢电梯在换乘楼层的冲突?
换乘楼层就是临界区,临界区应当配备一把锁。进入临界区就要获得锁,否则等待。离开临界区就释放锁。
4. 单双轿厢设计如何
见第四部分“四、架构设计和分析 - 第三次作业”。
四、架构设计和分析
这是代码架构分析,不包含任何解题思路。解题思路请看第三部分“三、解题思路”
第五次作业
架构设计
第五次架构设计比较简单,实现的功能也比较简单。整体设计从UML协作图中基本可以看的清清楚楚,个人认为没有太多地方需要特别叙述。解题思路
UML协作图
在网上搜了一圈,感觉协作图(通信图)五花八门。有利用时序图来画协作图的,有我下面这样的,有利用方块表示类的。我认为只要能够说清楚线程之间的写作关系就行,因此我打算在三次作业的协作图中使用下图的形式。
整体上就是一个生产消费模型,输(gong)入(chang)线程生产乘客,交给调(tao)度(bao)平台调度,最后交(mai)给电梯消费掉。
UML类图
复杂度分析
所有圈复杂度较高的方法我都看了一圈,事实就是if-else及其嵌套太多,用于各种条件判断。每一个if-else里面的内容就两行:一行设置下一个状态,一行return。我当然可以拆开这些if-else,然后把每一次嵌套都封装成一个私有方法,但是我并不认为这会让可读性变高,反而可能会过度封装。此外可以发现,复杂度过高的方法其实都被私有化了。
我承认Strategies类写的确实不太好,不过我已经不知道怎么优化了。至于Elevator类和WaitQueue类我倒是不是很担心,我在写一个类的时候会根据外界需求在里面封装好许多好用的方法,哪怕这些方法可能并不会用到,就有点像自己搓一个小的库一样吧。这使得方法复杂度很高,但是实际上既不影响可读性,我也不认为与封装的思想相矛盾。在之后的作业中我沿用了这个类,导致复杂度一直降不下来。
另一方面,由于我设置了UP和DOWN两个状态,导致几乎所有和上与下有关的代码都会高度相似、对称。后面我了解到原来枚举类型也是可以设置方法的,这样子我或许可以将这部分代码简化不少。
第六次作业
整体上分了三类线程类和三类共享对象,具体如下图所示。个人认为整体还是比较清晰的。
仍然是策略类的方法复杂度过高,过高原因同第五次作业相同:单纯因为需要很多的条件判断。但是整体上可读性还是蛮高的:各种if-else
判断整体来说看得还是比较清楚的。
其次复杂的事Elevator
类中的actWait()
和run()
方法。actWait()
由于设计遗留的历史原因,他和run()
方法其实有一部分代码高度相似。当时在写的时候想了半天没有想明白应该怎么设计多线程的终止条件,于是写了一个很复杂的终止条件判断,最后结果是放在Elevator.run()
也觉得怪,放在actWait()
中也觉得怪,所以就把两边的代码都保留了。实际上终止条件并没有这么复杂,不过我在写完第三次作业之前都不知道要怎么设计,算是一个小遗憾吧。
SchStrategy
的 onTheWay()
方法由于时间问题,虽然写了但是没有启用,姑且不认为它复杂。TestMain
是用于测试的代码,未提交至平台上。
WaitQueue
的复杂度来源于各种不同限制条件下的查询和检索:查询高于某一楼层的希望向上走的乘客、查询该层楼下所有向下的乘客等等。每个方法并不复杂,但是由于方法实在太多,复杂度不得不上来。
Elevator
类的主要复杂度就是上面提到的几个复杂度较高的方法。删去后方法复杂度马上降下来了。
EleStrategy
的方法复杂度和第五次作业的Strategies
复杂度完全一致,不过多叙述。
第七次作业
第七次作业的主要改动在于封装了电梯。
在第五次和第六次作业中我一直是让线程对象直接调用共享对象中的方法来完成添加乘客,删除乘客等操作。但是后面发现似乎并不一定需要直接调用共享对象的方法,而可以将共享对象封装到其他对象中,调用其他对象的方法来完成。
比方说,如果调度器要将乘客加入到电梯中,我们会说:“把这个家伙分配给这个电梯!”而不会说:“把这个乘客放到这个电梯的等待队列中。”对于调度器来说,前者的思想更符合我个人的直觉。
这有什么区别呢?前者我们将电梯的等待队列的概念隐藏了,电梯对于外界而言没有等待队列的概念,外界只知道自己将一个乘客分配给了这个电梯。但是后者却将电梯的等待队列暴露了出来。
直接上协作图吧。
关系个人认为还是很清晰的。主要是添加TrashCarrier
现成将电梯完全封装了起来后结构变得十分清晰。
类图则稍微复杂了一点。我删除了所有私有化方法(继承接口是一点不会啊)
从中可以看出虽然加入了Elevator
中间层,但是这没有降低多少设计复杂度。单轿厢类、电梯线程类和电梯中间层类应该有更清晰的层次关系,但是当时设计没有想太多,于是就写成这样了。
EleStrategy
即为先前的Strategy
和Strategies
类,复杂原因已经在前文中叙述过,在此不再叙述。
TestMain
是测试的一部分,不属于项目,也不会提交至评测平台。
ElevatorThread.run()
的主要复杂度来和第六次作业一样,源于结束条件的判断。不过相比第六次作业已经有了很大程度上的降低。
Elevator.outputString()
是一个用于输出的静态方法。对于单双轿厢电梯,双轿厢的上下层电梯都需要特判后才能进行输出,这导致设计的圈复杂度增加。
EleStrategy
的复杂度同第六次和第五次作业一致,不再过多叙述。
ElevatorThread
的复杂度是我没料到的,我坚持认为它不复杂。大概就是:认知复杂度高的方法essential圈复杂度
都很低,
五、调度策略选择
这一部分是关于具体的调度器调度策略,以及电梯的调度策略的叙述和我的选择。
经过第一单元的淬炼,我想第五次作业应该会有各种各样的神奇策略能够被想出来。但事实就是没人这么干。大多数人使用的都是LOOK
策略。这里叙述几种常用的策略和我的选择:
第五次作业
第五次作业调度器没有任何调度策略。调度仅发生在单个电梯中
1. ALS调度策略
我至今也不知道ALS
到底是什么的缩写,但是只要在互联网上搜索“ALS调度策略”,我们一定能找到北航计院的身影。
ALS
的调度策略很简单:
- 电梯有人时,谁先到电梯就先送谁
- 电梯没人时,去接最早来的人
- 同向能捎带就捎带
ALS
的缺点很明显: - 首先,尽管有主请求的定义,但是当电梯为空的时候,主请求很容易被捎带请求覆盖。因为只要一个空电梯捎带了一个人,那么被捎带的人就是主请求。所以这并不能避免等待时间长的人有更长的等待时间。
- 主请求容易诱导电梯,让电梯实际上并没有那么公正的策略,而是成为了一个谁手快谁占据更好的位置谁就更容易搭上电梯。
- 电梯的不公正的接客原则还使得电梯运行路程可能会更长,使得耗电更多。
ALS
代表了一类算法,或者说一类算法的原则:优先接最早到达的人。这是ALS的整体原则,也是我们在设计时可以考虑的原则。
但是ALS
并不是那么好。比如:
Example 1: 假如有一大波同学1楼到9楼,他们先按了电梯;而有另一波同学想从10楼到1楼,但是他们全部都按晚了电梯。
根据ALS
,他们的请求不会被响应,直到前一波人的请求全部被完成—。当电梯到9楼,电梯宁愿去接最早到的乘客,也就是1楼的乘客,也不愿意接10楼的乘客——哪怕电梯只需要上行一层楼!
事实上,不加改进的ALS
策略的实(qiang)际(ce)表现也不佳。这就出现了SCAN
策略和LOOK
策略。
2. SCAN/LOOK策略
回想一下我们现在乘坐的电梯,在Example 1
中二楼的乘客每次上电梯的时候应该会发现有一大波人从电梯里涌出来。这是因为电梯采用了LOOK
策略。在讲LOOK
之前,先讲一下SCAN
策略。
SCAN
策略做的事就是在最高层和最底层之间来回扫。SCAN
没有主请求,但是顺带着却能够接到更多人。在Example 1
当中,他们会先去接10楼的乘客——不是因为10楼的乘客优先级有多高,只是因为电梯会运行到最高层(11层)再往下运行,而往下运行的时候就正好能够捎带10楼的乘客。
但是11楼是没有必要去的,因为11楼没有人。这就引出了LOOK
策略。**LOOK
策略意在解决SCAN
中的无效运行。在LOOK
中,我们只需要找到最高和最低的有需求的楼层就行了**:不需要到11楼,而是到10楼就行。
通过这个例子我还想说明一个问题,那就是只做大优化不做小优化。当你以为LOOK
比SCAN
好的时候,请思考这么一件事:如果在LOOK
接到十楼的乘客并且开门了打算往下走,这时如果有一个十一楼的乘客刚刚按电梯,那这个十一楼的倒霉蛋就要在等一轮了。也就是说,无用的运行有可能会有用,这取决于具体输入。不要为一些小细节优化,总是要尝试进行大优化。
LOOK
策略是一种非常好的策略。在第六次和第七次作业中,由于调度器会将乘客分配给不同电梯,很难出现一个电梯会有很多很多人的情况(数据限制最多100个乘客,平均下来每部电梯不超过17个乘客,很快就能送完)。所以LOOK
的弊端再次看来并不会这么明显。
3. LOOK + ALS
LOOK
和ALS
不仅仅是两个具体策略,他们更反映了两个思想方法或者原则:一个认为最早来的人最应该先响应,另一个认为我自己走我的路,方便我响应我就响应。LOOK
原则和 ALS
原则各有优劣:LOOK
很容易使某一个人等待时间过长,而ALS
可能会对某一些按按钮晚的人很不公平。
这两个原则可以结合一下,取长补短。比方说,我们可以优先响应同方向上到达时间最早的人,在保证能够接到它的前提下再去捎带其他人。这样子在一个方向上很难出现等待时间过长的人,进而会更好。
4. 我的算法
我认为LOOK + ALS
并不足以满足乘客的需求,因为这种算法只能考虑当前情况下等待时间最长的人,却不能考虑当前情况下多个等待时间很长的人。当一个方向上有很多等待时间很长的人的时候,而我们在没有接到他们之前接了其他捎带的人,那么这时候我们很可能就只能接到一个等待时间很长的人,也就是主请求。而其他人等待时间明明也很长,却因为在电梯接到他们之前捎带了其他人,因而无法搭乘电梯。
坏心情算法认为造成这样的原因是因为主请求太少了。 如果有多个主请求,我们就可以先保证先去接这些主请求,再去考虑是否捎带。
在我的算法中,请求的优先级是根据乘客的心情的好坏来决定的。我规定,如果电梯在一次捎带过程中明明可以和我同方向经过了我的楼层却不捎带我,那么我的坏心情就+1,也就是我的优先级在电梯眼中就提高了1级。这种算法来源于使用新北电梯的真实感受,我把它叫做坏心情算法。
坏心情算法解救了LOOK + ALS
算法中两个等待时间较长的人可能只能接到一个的问题,实际上如果稍微更改一下坏心情算法,将时间而不是坏心情作为最高优先级,我们就得到了另一种LOOK + ALS
算法了。
坏心情算法的缺点也是很明显的:它几乎完全被坏心情的人带偏了。如果坏心情的人比较多,并且这些气急败坏的人的请求比较苛刻,那么它们可能会占据电梯的所有位置,导致捎带的人减少,进而导致电梯要花费更长的时间才能够送完所有乘客。
5. LOOK的贪心改进
方法提出自研讨课同学N Boy
。膜!
传统LOOK
认为只接同向可捎带的乘客。但是实际上在乘客较少的情况下,LOOK
可以两个方向上的乘客都接上,以减少开关门时间。
6. 总结
没有最好的调度策略,所有的调度策略都有反例,因为未来是未知的,人流量、目的、到达时间就无法纳入考虑因素当中。再退一步讲,即便我们能够知晓未来的一举一动,判断出最佳策略也是困难的。因此经过第五次作业后,我同意指导书的观点,最好的策略就是变换策略。不过真的麻烦死了……
第六次作业
在第六次作业中,乘客不再指定进入电梯号,而是由同(wan)学(jia)自行调配。因此在第六次作业中有两个策略需要完成:一个是把乘客分配到不同电梯的策略,另一个是电梯接送乘客的策略。由于电梯在没有RECEIVE的时候不能移动,因此不能让电梯自由竞争乘客。
第六次作业中的调度策略整体来说大概有这么几种构调度策略/原则:
1. 123456
顾名思义,把乘客依次分配给1, 2, …6号电梯。性能十分均衡,性能分也很一般。
2. 随机调度原则
随机将乘客分配到不同的电梯。实测随机调度大概能够在强测中获得
调度的随机性意味着得分的随机性,不清楚课程组会不会反复测试代码然后取平均,但是本地运行多次可能会有不同的效果。乘客量和乘客密度大的时候几乎不会有分数波动。
3. 捎带原则
方便捎带就能捎带,结合LOOK
类算法可以有不错的性能。不适用于坏心情这一类复杂的算法,理由是很难计算能不能捎带这个人。
4. 影子电梯
实际上,根据现有的所有信息,我们是能计算出乘客应该分配到哪一个电梯最好的。我们只需要模拟一遍电梯的运行,算出把乘客分配到哪个电梯更好,然后分配给电梯就行。而且这个分配的时间是由调度器完成的,意味着分配不会占用电梯运行时间。这是最理想的贪心算法。由于模拟过程好似有一个和实际电梯完全一样的电梯在运行,所以称模拟的电梯为影子电梯。
影子电梯的模拟也是分好坏的。优秀的影子电梯可以做到强测
影子电梯也有模拟不到的地方,比如很难将RESET
用时考虑进影子电梯中。
单电梯的调度策略和上次一样,LOOK
就行。
第七次作业
几乎没有变化,本人第七次作业摆大烂。至于调度策略,基本上同第六次和第五次作业。
六、Bug & Debug
遇到的Bug
1. 电梯的等待队列waitQueue不是线程安全的
解决方案:使用同步方法。
2. 电梯为空时无法决定新的方向
解决方案:那就根据LOOK
策略选择一个方向。
3. 同步方法过多
解决方案:分析线程的关系,只在容易产生冲突的地方进行同步;私有化方法
4. git commit –amend使用不熟练
解决方案:自己手动解决冲突就行,应该相当于commit一次再与上一次提交merge。
5. 疑似非法提交
我为不同类写了一个debug()方法,但是问题是难以判断调试信息输出的位置。搜索后发现下面这行代码可以打印当前Thread运行位置。评测中检测非法字符串的时候检测到了这一行代码。
1 | System.out.println(Thread.currentThread().getStackTrace(](1].getMethodName()) |
6. RESET和RECEIVE冲突
这在第六次作业和第七次作业中比较常见。RECEIVE
不能再电梯的RESET
期间输出,否则错误。第六次作业误打误撞把冲突解决了,导致第七次作业出现这个报错的时候我都不知道原来还有这项要求。
解决方案:加一把锁,RESET
期间持有锁,完成RESET
后释放。简单来说就是电梯必须在外界看来永远都是在正常运行的。如果在不正常期间运行的话,外界对电梯的访问就会被阻塞住。
注意事项:另一种解决方案是通过查看电梯的运行状态来判断要不要等待,如果电梯在RESET
状态就等待,直到被notify()
。这样的实现是不稳妥的,很有可能在你查看状态的下一瞬间电梯就完成RESET
并且notify()
,这样子就查看电梯状态的线程就永远不可能醒过来了。如果一个线程的运行依赖另一个线程的状态,那么最好用锁。
7. 未RESET_BEGIN就重新分配等待队列中的乘客
在RESET_BEGIN
之前电梯需要把分配到这部电梯的乘客交给调度器,调度器会调度乘客,乘客会被分给新的电梯并输出RECEIVE
。这看起来没什么问题,但是这一切都可能在一瞬间完成,然后下一瞬间电梯才输出RESET_BEGIN
!意味着电梯还没有RESET_BEGIN
时,另一个电梯就抢走了原本电梯的乘客! 第六次和第七次作业不允许出现这样的行为。
解决方案:我的解决方案比较奇葩。我会先让乘客从电梯轿厢中假装出来(输出开关门和出电梯但时在对象的数据中仍然存在于电梯轿厢中),然后输出RESET_BEGIN
,再把乘客送给调度器。
8. 多产品的生产消费模型
电梯的重置RESET
有至少两种实现方式:一种是将RESET
作为一种属性写入电梯中,电梯每次切换状态需要查看是否处于需要RESET
的状态,需要则RESET
。但是如果有多个RESET
指令同时涌进来的话,前一条RESET
指令就会被覆盖掉,不过由于第六次和第七次作业的约束(同一电梯的两条RESET
不能有太近的时间间隔),这种情况几乎不可能发生。
另一种实现方式是将RESET
看成另一种产品传递给电梯,构建区别于乘客这种产品的另一条生产-消费供应链。这就产生了多产品的生产消费模型。这给控制设计带来了不少的麻烦(自己写一遍就知道问题了)。
解决方案:让多产品的输入共用一把锁,使用Check-and-Act
行为进行设计。
9. 停不下来
解决方案:具体停不下来的原因千奇百怪,设置合适的终止条件基本就能停下来了
Debug策略
多线程Debug
是一件麻烦事,不过IDEA
帮我们做了不少工作。IDEA
中右键断点的红点点可以选择默认多线程Debug
,因而在每一个线程运行到这一行代码的时候都能够停下来(不过我倒是很好奇gdb
有没有这种功能)。缺点就是实际上运行时不会有线程停下来,所以导致运行和调试可能带来不同。即便如此,Debug
还是麻烦的:Debug
效率本来就低,现在还要同时在六个电梯线程中打断点,这我真的受不了一点。所以我直接print
大法调试。我在每一个我需要用到的类里面写了一个debug()
方法,用于往终端输出该对象的信息。另外建议将printf
大法打印出来的信息直接输出到标准错误输出,因为IDEA
中标准错误输出是红色的。
在第六次和第七次作业中我写了一个Debug
类(里面全是静态方法),所有的printf函数都是调用Debug类的静态方法完成的。并且在Debug
类中设置了一个参数valid
,输出当且仅当valie = true
,否则不会输出debug调试信息。
七、心得体会
首先印象最深的是关于synchronized
关键字。synchronized
的同步语句块或者同步方法只需要在共享对象上使用,或者更一般地,只需要在多个线程产生访存冲突的地方使用。合适的synchronized
可以更好地保证程序的正确性和安全性。在第五次作业中,由于滥用synchronized
关键字,我得到了非常糟糕的第五次作业初稿。
此外,这次作业也算是掌握了一些基本的多线程编程的基本思想。生产-消费模型是多线程或者并行编程中的一个最基本的模型,电梯分配属于他们的一个简化版本:没有产品上限,不需要考虑生产消费速度,也几乎不需要考虑某一个电梯闲置时间过长的情况(顶多强测分数低一点)。此外,JAVA
提供了非常舒适的多线程编程方法,在其他并行编程模型中不一定会有这么舒适的选项。
最重要的,应该是对多线程有了更深的理解吧。个人感觉这种多线程编程、并行编程的编程方法,在日后的编程学习中应当会有非常好的应用,而且不一定会有机会再接触到了。
八、课程建议
不知道第三单元和第四单元学些什么,但是第二单元多线程确实蛮有用的,如果后两单元没啥大事的话个人认为其实可以把这一单元延长一点…点。
(好像还是说了很多废话…)
- 标题: 「BUAA OO」Unit-2 电梯调度
- 作者: Squirrel7ang
- 创建于 : 2024-03-31 18:55:00
- 更新于 : 2024-07-21 22:16:23
- 链接: https://redefine.ohevan.com/2024/03/31/OO/Unit-2/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。