BUAA OO Unit2总结

王天一-23373193 2025-04-15 17:37:01

同步块与锁

第一次作业较为简单,是一个典型的生产者-消费者模型,我的共享对象只有RequestTable一个类的对象,因此我仅将RequestTable中的每个方法放入同步块中,每当需要修改请求表中的元素,就需要先获得它的锁。而对于每一个Elevator类的对象,他们都会获得一个与InputThread共享的RequestTable对象,如果当前该请求表为空,则在该请求表上等待。当该电梯的请求表接收到请求时,就会唤醒该电梯。

第二次作业由于“临时调度”请求的加入,电梯系统从简单的“输入线程生产、电梯线程消费”的模式变成了“输入线程和电梯线程都可能生产、电梯线程消费”。这造成我们需要新开一个MainTable类,用于存放尚未被分配的请求,而为了防止notifyAll()一次叫醒所有电梯所导致的CPU时间浪费,我为主请求表类设置了ReentrantLocknotEmptyCondition,当输入线程获得了请求或电梯需要“吐出”请求时,只会叫醒在notEmpty上等待的调度器线程。

第三次作业新增了“升级”请求,我们必须保证2个电梯均处于关门状态后才能开始进行升级。为了达到线程间通信的目的,我设置了CountDownLatch,让负责升级的线程先通知电梯线程进行开门放人操作,并在该latch上等待,当2个电梯均结束放人,负责升级的线程会被唤醒,进行升级操作。之后设置另一个latch,负责等待电梯完成升级操作,之后结束该升级线程。

调度器

线程交互

第一次作业中指定了运送乘客的电梯,因此调度器的实现较为简单。当调度器接收到来自InputThread的输入请求时,会将该请求加入到对应电梯的请求表中,同时唤醒该电梯。

第二次作业中,当MainTable接收到新的请求时会提醒调度器开始调度,调度器依据调度策略将该请求加入指定电梯的请求表,同时唤醒该电梯处理该请求。

第三次作业中,我大体上沿用了第二次作业中的线程交互方法,除了当接收到UPGRADE请求时会开启一个新的线程专门用于升级,其交互逻辑在上文已经说明,这里不再赘述。

调度策略

我在二、三次作业中的调度策略基本一致。具体来说,是通过深克隆6部电梯,然后模拟每部电梯加入当前请求后的运行状态,计算电梯将当前请求表中所有乘客送至目的地的总时间与总电量,并将请求分配给time + electric最小的电梯。我认为这样的调度策略能够实现局部最优解,得到基于当前请求的最佳运行策略。

不过我的影子电梯是有缺陷的,在hw7中,由于升级操作需要某两部电梯获得多把锁,为了避免死锁问题,我没有在深克隆电梯时为电梯上锁,这可能导致克隆电梯的状态与真实电梯存在差距,使得模拟结果不准确,是一点遗憾。

架构分析

img

主架构

第一次作业是典型的生产者-消费者模型,其中InputHandler用于接收输入请求,为生产者,Elevator处理请求,为消费者。Scheduler作为调度器,存储了所有电梯的RequestTable对象,当请求到来时,将请求按照分配策略添加到对应电梯的请求表中,同时唤醒电梯进行消费。

在三次作业中,我均采用“电梯与运行策略分离”的设计。策略类Strategy获得电梯当前的状态与其请求表的情况,运用“类LOOK”算法为电梯运行提供Advice枚举类中的一种。

Person类的设计是考虑到后续可能需要给请求增加一些信息,例如请求到达的时间、该人是否处于换乘状态、该人当前要前往的方向等。

SCHE

由于SCHE请求的加入,电梯也变为了生产者,那么每个电梯独有的那一个RequestTable就不够用了,所以我设置了MainTable类,用来接收输入线程和电梯线程产生的请求。当MainTable接收到请求就通知SchedulerScheduler根据模拟情况分配请求。

另外,题目要求电梯在临时调度期间不能参与调度,但为了保证性能,我依然需要为正在临时调度的电梯分配请求(试想如果所有电梯在同一时间临时调度,那么不给它们分配请求肯定是要完蛋的),因此我对电梯进行了改造,为其加入了buffer用来存储被分配的请求,并增加一个fromBufferToRequest()方法,当电梯能够接收请求时,将buffer中的请求移动至RequestTable中,同时输出RECEIVE

UPDATE

第三次作业加入了UPDATE请求,它的主要难点在于如何实现两个电梯线程之间的通信。在之前的设计中,我的调度器是所有电梯的管理者,因此UPDATE请求理应由调度器线程来处理,但由于升级需要耗费时间,调度器是一定不能由于一个升级请求而睡1s的,因此当调度器接收到一个新的UPDATE请求时就会开启一个UpgradeThread线程专门处理升级。该线程通过CyclicBarrierCountDownLatch实现两个电梯线程之间的通信、协调工作,同时不影响Scheduler线程继续进行请求分配工作。

三次作业时序图

第五次作业:

hw5

第六次作业:

hw6

第七次作业:

hw7

作业间对比

对于我的设计来说,电梯运行策略是稳定的,三次作业均采用一种运行策略,我认为主要原因是这三次作业对于电梯本身属性的要求没有改变。而调度策略是易变的,因为每次作业都有新增的独特需求,为了满足这些需求,我们需要去改变自己的调度策略,让调度器合理的处理这些请求,让电梯合理的响应这些请求。

双轿厢

同步改造

其实上面已经提到了,那这里不妨在写一遍:为了达到线程间通信的目的,我设置了CountDownLatch,让负责升级的线程先通知电梯线程进行开门放人操作,并在该latch上等待,当2个电梯均结束放人,负责升级的线程会被唤醒,进行升级操作。之后设置另一个latch,负责等待电梯完成升级操作,之后结束该升级线程。

避免碰撞

首先要明确的是,我的两个电梯依然是独立运行的,他们依然独享一张请求表,互相并不知道对方的存在。为了避免碰撞,我修改了它们在目标楼层时开门关门的运行方法。具体来说,当其中一部电梯需要前往目标楼层时,它需要先获得一把锁,如果成功抢到了锁,那么进行移动、开门、上下人、关门、移动这一系列操作,当完成最后一个移动动作的瞬间释放锁,让另一部电梯能够到达该层。

Debug

虽然我的程序非常幸运的在强测和互测中没有出现Bug,但自己测试时还是不少出锅的。其实与多线程有关的bug是少数的,例如hw5中在判断是否结束所有电梯线程时,忘记给检测RequestTable是否为空的方法上锁,导致线程提前结束的问题。大部分bug还是与电梯运行策略与调度策略有关,比较典型的运行策略bug是电梯反复原地掉头、电梯到达0层或12层、电梯在某几层之间反复运动(主要出现在hw7)、电梯反复开关门等神奇行为,他们的主要原因都要归功于对于请求表或电梯内请求的增、删、改的逻辑问题。调度策略的bug对于正确性影响不大,但会导致性能爆炸,比较典型的是将所有请求全部分给某部电梯,其原因是没有正确计算UPDATE对于电梯内所有请求造成的时间影响。

我的debug方法是print打法,由于遇到的线程相关bug不多,因此这个方法屡试不爽。我为主要的类实现了toString()方法,在调试时只需要输出改类的某个对象的状态,一般就能够发现问题所在了。

心得体会

线程安全方面,最重要的一点就是该加锁就加锁,不能因为害怕死锁就偷懒不加,本人就偷懒了,导致影子电梯模拟出现一些问题,性能分没有拿到很高。至于层次化设计,我认为我的Unit2设计远不如Unit1来的精巧,比如我的Elevator类已经来到了惊人的480行,显然是有问题的。但调度策略写起来太累人了,也就没有太多精力去好好进行结构设计了,这也是我的电梯的一点遗憾吧。

...全文
26 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

269

社区成员

发帖
与我相关
我的任务
社区描述
2025年北航面向对象设计与构造
学习 高校
社区管理员
  • Alkaid_Zhong
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧