269
社区成员




以大楼的电梯为背景,建立属于自己的电梯系统。
要求:
- 乘客搭乘电梯前自行填写目的楼层以及搭乘电梯,同时给出乘客的优先级。自行设计电梯运行策略,来实现电梯最终能将所有乘客送到目的地。同时考虑所有电梯运行时间,乘客按照优先级的加权等待时间,电梯耗电量。
- 生产者-消费者模型
生产者-消费者模型用于解决多线程环境下的生产者和消费者问题。在生产者-消费者模型中,生产者负责产生产品,消费者负责消费产品。同时设计一个托盘,生产者生产得到的产品放入托盘,消费者从托盘中取出产品进行消费。
- 锁的设置
在生产者-消费者模型中,生产者和消费者分别代表两个线程,托盘表示两者之间的共享对象。多线程对于共享对象的读写需要考虑同步互斥问题;对共享对象进行加锁,可以实现同一时刻仅有一个线程对共享对象进行读写操作;调用wait()以及notify()方法可以实现线程的等待与唤醒,从而解决同步问题。
在本次作业中,总共设计了三个线程:输入线程、电梯调度器线程以及电梯线程。
1、输入线程与调度器共享requestTable类,输入线程负责将乘客的请求放入requestTable中,调度器线程负责从requestTable中取出乘客的请求。
- 在解决此线程安全问题中,主要是对requestTable中每个方法进行上锁,构建一个线程安全类,来保证线程安全。
2、调度器和电梯共享等待队列waitQueue对象,调度器将乘客放入waitQueue中,电梯根据自身运行策略根据不同楼层的等待队列进行运行以及捎带决策。
- 在解决此线程安全问题中,一开始将waitQueue设计为LinkedList来实现根据优先级重新排列,但是LinkedList并非线程安全类,因此在对waitQueue进行操作时,对waitQueue对象进行上锁,使用同步块的方式来保证线程安全。
3、为了避免轮询问题,电梯线程当等待队列中没有乘客,同时电梯内也没有乘客时,电梯线程会调用wait()(以等待队列为对象设置锁),进入等待状态,直到调度器将新的乘客放入等待队列中,同时调用notify()(依旧以等待队列为对象设置锁),唤醒电梯线程,从而避免电梯线程的轮询问题。
总共实现了两种运行策略:LOOK、 ALS;
- LOOK策略:总结来说,LOOK策略是以电梯运行方向为重点指导;当电梯在等待时,根据第一个进入等待队列的乘客的等待楼层,来决定电梯的运行方向;此后当电梯运行过程中,会将途中同方向乘客进行捎带,直到将此方向的乘客全部送达完毕后,再根据新乘客决定新的运行方向。
- ALS策略:ALS策略是以乘客优先级为重点指导。当电梯内无人时,电梯根据所有乘客中优先级最高的乘客的等待楼层设定电梯运行方向,若有同方向可以进行捎带;当电梯内有人时,以电梯内最高优先级的乘客的目标楼层设定电梯运行方向。
- LOOK策略在乘客较多的时候表现较好,可以使运行时间更短;ALS策略在乘客较少的时候表现较好,可以更好地实现电梯根据优先级的调度的要求。
- 设计strategy接口,LOOK以及ALS分别实现strategy接口,在电梯运行时可以更灵活地选择不同的运行策略。
- 1、设计Person类,用来记录乘客的各种信息。
- 2、设计Queues类,用来管理waitQueue以及exitQueue等队列。(很可惜,直到第二次作业才发现Queues类也可以设计成一个线程安全类,此次作业并未设计,导致电梯线程中代码较为冗余)
- 3、设计枚举类DIRECTION、ADVICE,分别用来记录电梯的运行方向以及电梯的运行策略。
- 4、设计Route类,来管理电梯停靠楼层等信息。
从协作图可以看出不同线程之间的创建顺序以及关系,但其中Route和Queues并非线程,而是在这两个类对部分方法进行了上锁,来实现线程安全,故体现在了协作图上。
本次暂无bug。
相比第一次作业,移除了乘客指定电梯的要求,同时增加了电梯临时调度的请求。
- 均分策略:相对容易实现,六个电梯依此分配。
- 影子电梯:每新来一个乘客,则对六个电梯此时的状态进行复制,并通过模拟电梯运行的方式,通过加入此乘客,来计算最终运行时间,选择运行时间最短的电梯。
- 影子电梯带来的新问题就是引入了新的共享对象,即电梯的状态。同时引出这是同步问题还是互斥问题的思考:应该是互斥问题。
- 1、身为调度器,其不应该会阻碍电梯的运行,尽管调度器中仍然在模拟计算,电梯不应该等待每次调度器模拟结束后再运行,这甚至是不符合常理的。
- 2、但是不设置为同步会引起模拟过程中电梯的状态不准确,比如在模拟过程中,有一个电梯接收到临时调度请求,但是模拟中并未考虑临时调度,导致有可能将乘客分配给一个实际运行时间较长的电梯。
- 综合考量下来,1的影响将显著高于2,1在每次有新乘客时都会引起性能损失,而2只在有临时调度进来时,才有可能引起性能损失,这个损失的量将远远小于1.
本次bug有:
- 1、在修改了普通电梯遍历中删除的bug后未同步到影子电梯中,导致影子电梯出现bug。
- 2、在进行处理临时调度行为中,尽管写了forceOut方法,结果最终漏写了这一方法,导致没有强制所有乘客退出电梯。这其实与正确性检测有极大的关系。
在第二次作业的基础上,新增update请求,收到请求的电梯强制退出所有乘客,原地进行改造。改造后形成双轿厢电梯(两电梯共用电梯井,且拥有一个公共楼层,因此需注意电梯碰撞问题)。
与第二次作业相同,对于update的请求,也是新增一个Updater线程,将update分别发送给不同电梯线程。
电梯解决update请求的行为几乎与第二次作业的sche一样,唯一有区别的是update Begin和End的输出:Begin需要两个电梯将所有乘客强制退出的时候输出,End需要两个电梯都完成开关门的要求后输出。因此设计了一个UpdateProcessor线程来接受两部电梯的信息,当一个电梯准备好begin或end的时候,会向UpdateProcessor更新自己的状态,当UpdateProcessor判断现在可以进行begin或end,再向两个电梯发送新的信号。同时在UpdateProcessor输出update begin和end。
第二个需要解决的问题是双轿厢电梯有可能相撞的问题。同上,设计了电梯井Well的线程来处理此方法。电梯若要进入共享楼层,则像此线程发送请求,若Well判断公共楼层有电梯占用,则向此电梯发送leaveSignal信号,当电梯离开,再向原请求电梯发送信号,让其进入。
- 实现方法: 每个双轿厢电梯对应一个电梯井,电梯井为中间楼层设置一个ReentrantLock,同时设置两条件WaitCondition以及OccupyCondition,来解决两电梯(发送请求-等待-Well发送leaveSignal-电梯离开公共楼层-Well notify原电梯-原电梯进入公共楼层)的同步问题。
- 分配策略主要实现了均分策略以及影子电梯策略,通过多次对同一输入数据对比发现影子电梯效果显著的好。
- 运行策略主要实现了LOOK以及ALS,LOOK策略在乘客较多的时候效果显著,ALS策略在乘客较少的时候效果显著。
- 1、多线程交互模式:
- 生产者-消费者模式保持稳定: 调度器分配请求时都是使用生产者-消费者模式。
- 处理update时使用了观察者模式,电梯每次有关update的状态更新,都会汇报给观察者,由观察者判断下一步行为
- 单例模式,对所有乘客进行统一管理,通过判断乘客是否全到达目标楼层来判断乘客调度器是否结束。
- 2、电梯运行策略保持稳定:Look以及Als在第一次作业形成后,后两次作业无需更改。
- 3、队列类保持稳定。
- 1、调度器分配策略易变:
- 均分策略中,需要考虑第三次作业引入的双轿厢电梯,当乘客的等待楼层该电梯无法送达时,则需要跳过此电梯。
- 影子电梯中:也需要考虑双轿厢电梯,在计算时间的时候,需要多考虑双轿厢电梯之间换乘的时间。
- 2、楼层类会改变,也是因为双轿厢电梯的引入。
- 3、电梯的行为也需要改变,主要由于新增的sche、update以及进入公共楼层等行为需要不断添加新方法。
与室友进行合作,一个室友负责制作有策略的数据生成器,涵盖同一时间多请求、多大幅度跨楼层请求、多sche、多update等策略。
我负责写正确性检测,主要通过输入记录所有请求,根据输出的结果一步步更改每个电梯以及请求的状态,在此过程中判断是否有违反题目正确性的内容;同时在输出结束后,对所有请求判断是否正确完成。
还有一位室友自己根据输出设计了一个可视化的debug工具,我们可以直观看见电梯运行、乘客的状态。
最后为了检验正确性,结合AI写了一个多线程十分简陋的评测机,主要结合室友的数据生成器指定输入,然后通过多线程一次性跑50次数据,同时限定运行时间,最后结合自己写的检查正确性来debug。(可惜自己写的内容也有一个正确性漏检测,刚刚好把自己第二次作业hack了...)。
- 上锁主要保护的是读与写或者同时写问题;wait与notify主要解决的是线程间的同步问题。通过上锁实现线程安全;同时也可以设计线程安全类来实现线程安全,从而避免大量加锁引起代码过于复杂。
- 主要是要明确多个线程的作用以及功能,其次明确不同线程之间如何交互。
- 然后为了避免轮询问题,也需要使用wait方法来停止不必要的空转,这时候更需要把握何时通过其它线程来唤醒此线程。
- 最后在线程的启动以及结束时机也需要把握好,基本思路是从顶向下式的启动线程,最后从底向上式的结束线程。