OO第二单元总结——电梯调度

OO第二单元总结——电梯调度

(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

第五次作业

本次作业我没有在各线程中设置同步块。在本次作业中,我一共设计了三个线程类:

  • Manager:电梯管理、执行线程

  • ReadPassenger:输入读取线程

  • TimedInput:定时输入线程(本地测试时用)

其中TimedInput类用于程序在本地运行测试时,根据规定的时间向ReadPassenger发送相应输入。在提交评测的正式作业中,该类不被启用。因此,实际运行中的线程只有ManagerReadPassenger

在两个线程间,共享的成员为WaitTable,是一个全局的候乘表。ReadPassenger线程向其中放入读取到的请求,若其非空或输入还未结束,Manager从其中取出请求并执行,否则Manager线程结束。两线程间无其它共享变量。

有鉴于此,我把WaitTable中各个涉及其内容的方法均设置为synchronized修饰的方法,包括向其中放入请求的put、取出请求的callOneIn、检查是否为空的isEmpty、设置输入已经结束标志的setEndOfInput、检查输入结束与否的isEndOfInput等方法。同时,由于Manager在空闲时需要挂起,我在WaitTable内设计了一个synchronized修饰的setCurToWait方法,其内部只有一个this.wait(),用于挂起调用该方法的线程。

在这样的设计下,我的WaitTable类成为了一个线程安全类。其锁加在实例化的waitTable实例自身上,任何对其内部元素的访问与修改都通过同步方法进行,保证了安全性。

第六次作业

本次作业我没有在各线程中设置同步块。在本次作业中,我一共设计了四个线程类:

  • Manager:电梯调度管理线程
  • Elevator:电梯线程
  • ReadPassenger:输入读取线程
  • TimedInput:定时输入线程(本地测试时用)

其中TimedInput类同上,用于程序在本地运行测试时,根据规定的时间向ReadPassenger发送相应输入。实际运行中的线程类为ManagerElevatorReadPassenger

本次作业相对于上次作业有较大修改,主要是将原来的Manager中的电梯运行部分抽离出来,成立一个新的Elevator类,以满足多部电梯的要求。而Manager成为一个纯粹的电梯调度器。本次作业中多线程共享的对象为:

  • WaitTable:全局候乘表,共享范围相比第五次作业扩大到了所有线程,以满足有时需要电梯对调度器进行唤醒的需求。
  • ElevatorQueue:单部电梯的等待队列。共享范围为调度器线程和该队列对应的那一部电梯线程。用于调度器Manager向电梯Elevator派发请求、电梯从中取出请求、电梯挂起和调度器唤醒电梯。
  • BlackBoard:记录电梯状态的黑板。共享范围为所有电梯线程和调度器线程。各电梯向其中写入自身当前方向、楼层信息,调度器从中读取这些信息。

本次作业中WaitTable类的设计没有太大变化,仍为线程安全类。

ElevatorQueue类中除了构造器外的所有方法均设置为了synchronized,以保证线程安全性。BlackBoard类也是相同设计。

第七次作业

本次作业我没有在各线程中设置同步块。本次作业在上次作业的基础上,主要的改动在于电梯类型、调度策略等,在几个线程之间的共享模块上基本没有改动。

(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互

第五次作业

本次作业我没有设置较为完备的调度器。由于只有一部电梯,我将调度器和电梯整合为一个Manager类,在其中执行LOOK算法。因此,电梯的运行、开关门、上下人都是在Manager中模拟的。

第六次作业

本次作业我设计了单独的调度器线程,主要工作如下:

每部电梯内部人数和在其等待列表中,将会被该电梯接走的人数相加不应超过6人,否则电梯状态为Full

调度器遍历当前在全局等待的所有乘客,并逐一检查电梯情况,若电梯为空,或者电梯未满的同时运行方向与此人准备前往的方向相同,并且电梯与他间隔大于一层(对应现实生活中需要让电梯停下来的距离),则记录下当前电梯。最后从所有可以允许该乘客进入的电梯里选择一部当前人最少的,将该乘客放入该电梯等待列表。

若遍历一遍发现当前所有乘客均找不到可以接受他的电梯,则挂起调度器,等待某个电梯下人,或者空闲时通知调度器,再将调度器唤醒,重新按上述方式尝试分派乘客请求。

在分派、调度过程中,调度器通过与其它所有线程共享WaitTable实现全局乘客请求的接收,以及其它各个线程对调度器线程的唤醒。其中,ReadPassenger线程读取到乘客请求就写入WaitTable,调度器每次从WaitTable中取出乘客尝试分派,若分派失败再塞回WaitTable。调度器本身需要挂起时也在WaitTable上挂起,而其它所有线程认为自身状态有所改变,需要通知一次调度器时,在WaitTable上执行一次notifyAll()操作。

通过与每一个电梯共享一个单独的ElevatorQueue,实现调度器和该电梯之间分派请求,以及需要时唤醒该电梯。调度器将请求放入ElevatorQueue,电梯从中取出请求。电梯自身需要挂起时在ElevatorQueue上挂起,而调度器认为需要叫醒电梯(如有新请求)时,在ElevatorQueue上执行一次notifyAll()

通过与所有电梯共享一个BlackBoard,实现调度器对所有电梯状态的掌握。每次电梯改变自身状态时,均同时修改BlackBoard中对自身状态的记录。调度器需要知道电梯情况时就去BlackBoard中请求所需信息。

第七次作业

本次作业的调度器设计大体与第六次相同,主要区别在于:

  • 在调度前增加了一个换乘步骤,若当前乘客未换乘过,则尝试设置一次换乘。换乘策略事先写好,为固定策略。之后,不管是否成功设置换乘,均把该乘客标记为已换乘。
  • 查找可用电梯时,增加一项检查当前电梯停靠楼层是否符合当前乘客所需的楼层。
  • 每次调度时,选择电梯优先选择C类电梯,若没有则选择B类,最后才考虑A类电梯。

电梯下人时会检查该乘客是否已到达本来的目的地,若未到达,则将乘客重新放入全局需求的WaitTable内。因此,不能再像上次作业那样,调度器分发完所有需求后就直接下班。本次作业,BlackBoard中增加了对电梯状态的记录,而调度器下班的条件相应改为:当全局不再有请求,且输入已经结束,且所有电梯均运送完毕挂起,调度器才能结束自身运行下班。这也给后续Debug埋下了隐患。

(3)从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性

UML类图

第五次作业

第六次作业

第七次作业

UML协作图(第七次作业)

可扩展性分析

考虑新增电梯类型,则在电梯增加的部分可扩展性良好,需要给出新电梯的类型、速度、楼层信息即可。但是,在换乘设计部分可扩展性较差,需要重新设计换乘方案,整个换乘计算方法transit()需要重写。

考虑新增楼层,则相应修改电梯停靠楼层即可,但是对于换乘而言也需要进行较大修改。

本次作业可扩展性不够优秀的原因在于换乘设计为静态方案,而非动态决定换乘,因此需要事先计算好换乘位置。一旦换乘策略所依赖的电梯类型、楼层、速度等发生变化,整个换乘策略就需要重新推导。

复杂度分析

方法复杂度

由于方法过多,只截取方法复杂度统计表最后一页。可以看到几个run()方法复杂度均较高,应该是因为其中包含了整个运行过程。

dispatchToElev()hasRequest()isNextMovable()nextDirec()等方法复杂度较高,原因是内部有较多的条件判断语句来决定接下来的行为。

类复杂度

类复杂度中,除了MainClass外,就是三个主要执行的线程复杂度较高,特别是作为调度器的Manager,其承担了大量判断工作,因此复杂度居高不下。

(4)分析自己程序的bug

第五次作业

我没有被发现bug。

第六次作业

强测中我被发现两个bug,互测未被发现bug。

强测两个bug分别为:

  • 当电梯空时向电梯内派发请求时未检查电梯容量,导致某电梯空下来被连续塞了二十多个请求,其它电梯空闲,二十多个请求由该电梯独自完成,最终超时。解决方案是加入对电梯容量的检查。
  • 装所有电梯的HashMap容器elevators和电梯等待列表容器elevatorQueues也在ManagerReadPassenger两个线程间共享,但是没有进行同步保护,导致在Manager内对列表遍历读取时,ReadPassenger处收到加电梯指令,向其中写入,报ConcurrentModificationException异常。解决方案是改用线程安全的ConcurrentHashMap容器。

第七次作业

强测中我被发现两个bug,互测未被发现bug。

强测两个bug表现为有乘客设置了换乘,但是下电梯后没有电梯再去接它。由于在本地屡次尝试复现均失败,尝试原样提交后通过测试......

此外,在课下自行测试时,我发现之前设置的电梯等待调度器唤醒,调度器等待电梯唤醒的关系会导致电梯和调度器均无法wait(),或均wait()。前者会导致轮询产生,后者会导致程序整体挂起,到210s时报Real Time Limit Exceed。最终考虑将调度器对电梯的依赖去除,改为调度器每次sleep(300),只让电梯等调度器的指令,打破了这样的依赖关系。

(5)分析自己发现别人程序bug所采用的策略

第五次作业和第六次作业我均采取自行编写Python评测机,随机生成数据进行测试,发现了多处同学电梯跑出1~20层的情况,以及有同学线程安全不到位,执行时报Runtime Error的情况。

第七次作业期间由于事情太多,没有跑互测......

本单元互测与前一单元互测的不同之处在于,前一单元的程序是简单的单线程程序,执行结果是确定的,与执行平台、环境等关系不大。因此,第一单元的测试仅需检测一次求导得到的表达式是否正确即可。而本单元的程序涉及多线程编程,其多线程部分依靠操作系统调度,而不同的执行环境,操作系统底层调度环境不同,就会导致出现不确定的结果。有时候可能某个bug触发的环境非常极端,以至于很难以复现,对测试者和维护者都是一种挑战。

(6) 心得体会

关于线程安全

  • 线程安全类的设计可以极大程度简化正常运行的线程的设计——当成普通的类用就好,但是带来的不便是在有些地方需要灵活加锁、加多个锁时,线程安全类就难以满足要求。
  • 要注意一些细节处的对象共享,比如不经意间在多个线程间共享的容器。可以考虑用Java自带的线程安全容器。
  • 不仅要关注不同线程之间的共享关系,还要关注它们之间直接的交流关系,在本次作业中主要是notify关系,这可能导致活锁的发生。

关于层次化设计

本单元的作业,第六次和第五次之间近乎于重写,原因就在于第五次作业没有贯彻好层次化设计的思想。第五次作业将调度器和电梯揉在了一起,第六次重新写了电梯和调度器。实际上可以考虑第六次单独写一个调度器,而把第五次作业揉在一起的调度器和电梯直接作为一个带内部调度的电梯,或许会更轻松一些。

在第六次把架构重新设计之后,第七次作业整体在架构上没有太多更改,主要问题在线程安全和活锁上。因此本次作业主要体验到的还是线程之间的协同问题。

小插曲

写评测机的时候,由于是多个同学的线程一起跑,最开始没有注意Python的深浅拷贝问题,几个同学的测试线程共用了同一个列表,也出现了严重的线程安全问题。没想到在这里又再复习了一遍深浅拷贝和共享。

posted @ 2021-04-25 11:58  涛父  阅读(125)  评论(1编辑  收藏  举报