第一次作业——FAFS调度
1.设计策略
第一次作业是单部电梯的傻瓜调度,所以电梯的运作不是难点。对于初次接触多线程的我来说,多线程之间的协同和同步控制是这次作业的难点。作业的整体思路我采用的是典型的生产者-消费者模式,请求输入是一个生产者线程,调度器相当于一个仓库,电梯则是消费者线程。
我采用的是共享对象来实现的多线程间的同步控制,输入请求线程中有一个判断输入是否结束的共享变量,调度器中有一个当前队列是否为空的共享变量。关于多线程的协同,我采用的是轮询的方法来控制电梯线程。第一次并不太会使用wait和notify等,导致采用暴力轮询的方法CPU占用率偏高。
2.基于度量分析程序结构
类图
这四个类的功能较为独立且简单。RequestInput 是输入线程,Dispatcher 是调度器,ElevatorRun是电梯线程。输入线程和电梯线程分别组合同一个调度器来实现线程间的交互。
傻瓜电梯的Complexity Metrcis
分析结果中可以看到ev, iv, v这几栏,分别代指基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(耦合度)(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度(独立路径的条数)。OCavg和WMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。
SOLID设计检查
-
单一职责原则(SRP):符合,每一个类和方法功能单一。
-
开放封闭原则(OCP):输入请求类是符合这一原则的,但电梯类和调度器类是不符合该原则的。第一作业的调度算法比较简单,对于之后优化的调度可扩展性不高。
-
里氏替换原则(LSP):由于没有使用继承,未能体现
-
接口隔离原则(ISP):由于没有使用接口,未能体现
-
依赖倒置原则(DIP):不符合,电梯类和输入请求类都依赖于调度器类,调用了其提供的一些方法,因此如果调度器类对外接口发生改动,则其他两类也需要改动。
3.BUG分析
在写这次作业的过程中,如何控制线程的结束真的难到了我。多次尝试,有完全没输出的时候,有程序无法结束的时候。最后在我debug半天之后(不过用的是轮询方法)总算解决了这个问题。本次作业真的很简单,实现之后测试是没有bug的。强测第一次得满分……
第二次作业——单部多线程可捎带调度(ALS)电梯
1.设计策略
第二次作业在第一次作业的基础上,大体框架是没有改变的,依然是生产者-消费者模型。但这次调度算法是ALS算法,线程间的协同与同步控制我抛弃了之前轮询的做法,采用了wait和notify等。
我的调度器内的队列一直用的是LinkedBlockingQueue,它是个阻塞的线程安全的队列,底层采用链表实现。
我采用的入队方法是put。put方法 :若向队尾添加元素的时候发现队列已经满了会发生阻塞一直等待空间,以加入元素
出队方法采用的是take。take方法看起来就是put方法的逆向操作。若队列为空,发生阻塞,等待有元素;队列不为空,从队首获取并移除一个元素
线程间的协同与同步控制依然是共享对象的方法。不过当队列为空时不再是轮询方法,而是wait,每当put操作就唤醒沉睡的线程,若从队列中读到NULL(LookAtFirst函数在队列为空时,若输入未结束则wait,否则得到NULL)则电梯进程结束。
2.基于度量分析程序结构
类图
相比于第一次作业,这次作业各个类的方法以及属性较多。这次我添加一个Button类,其实仿照现实生活中的电梯按钮。电梯线程有两个Button类,一个外部按钮,以楼层为索引统计各个请求;一个内部电梯,以楼层为索引统计电梯内的请求要到的目的地。关于该楼层的请求能否捎带等方法实现都是在Button类内实现的。因为增加了Button类,Button的信息也需要同步,故而额外增加了很多方法来实现其同步。
这样有好处也有弊端。有的人实现捎带,是在每个楼层遍历调度器类的队列,这样只需维护一个队列信息的同步,但这样每次遍历队列全部请求,也是一种浪费。我增加Button类可以使得操作变得更加直观,实际上调度器类的队列是按加入时间记录的,而Button类是按楼层来记录的。
ALS Complexity Metrcis
ElevatorRun类较为复杂,因为这次的调度算法不再那么简单,增加了很多方法来实现捎带。
ALS Dependency Metrcis
依赖度分析度量了类之间的依赖程度。有如下几种项目:
-
Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
-
Dcy和Dcy*:计算了该类直接依赖的类的数量,带*表示包括了间接依赖的类。
-
Dpt和Dpt*:计算了直接依赖该类的类的数量,带*表示包括了间接依赖的类。
SOLID设计检查
SRP:较符合,三个主要的类分别负责请求的获取输入,对请求的调度分配,电梯运行。
OCP:较符合。为了能够实现多部电梯的ALS调度,我给每部电梯扩展了外部按钮以及内部按钮的功能,使得每个电梯的运行变得独立。这样的框架使得我在第三次的作业只需改动调度器。
LSP:没有使用继承,未能体现
ISP:没有使用接口,未能体现
DIP:不符合,因为我的类与类之间存在组合关系仍然出现了高层模块依赖底层模块的现象。
3 .BUG分析
在自己运行了部分数据后,我的程序并没有出错。当然在逻辑上我认为也是没错的。不过多线程的BUG是手动测试测不出来的,对于没有评测机的我来说无论是测自己的还是别人的潜在Bug都比较困难。
4.电梯调度算法分析
其实这次作业我的性能分几乎为0。我采用的捎带方式即是指导书中所述的最原始的主请求捎带。我的调度在去接主请求的路上是不能捎带的。哎……都怪自己懒,性能分奇低。
我认为比较优化且符合实际情况的,即既可以尽量缩短总运行时间又可以照顾每个请求的用户体验的算法是SCAN算法。电梯除了静止的状态,总是上行之后下行,下行之后上行,即在一趟来回中解决尽可能多的请求。按照楼层顺序依次服务请求,让电梯在最底层和最顶层之间连续往返运行,在运行过程中响应处在于电梯运行方向相同的各楼层上的请求。
由此看来,我之前的捎带算法是靠主请求来决定电梯运行的方向的,而scan算法是靠运行过程中请求分布楼层情况来决定运行方向的。其实在我实现了Button类之后,scan算法是很好实现的。啊啊啊……,后悔当初没有勤快一点!!
第三次作业——多电梯
1.设计策略
第三次作业要求模拟多部智能电梯,并且存在转乘的情况。单独电梯我还是采用的ALS捎带调度算法。这次改动较大的是调度器类。
这次采用了两层调度的策略。对于需要转乘的请求,我在调度器内将其拆分为两个请求。然后让第一个请求分配进优先级较高的电梯。当某个请求从电梯内出来,我会判断该ID是否需要转乘,若需要转乘,则对其第二个请求分配入当前优先级最高的电梯。一个ID的拆分指令之间的协同,我是通过调度器类的一个Button类来实现的。其实际为一个以ID为Key的hashMap,只有当前ID的第一个请求完成时,才能将第二个请求分配入相应的电梯队列(这次调度器类有三个电梯的队列。)
电梯运行结束的控制条件为输入结束且调度器类中的hashMap为空。这样我就能保证完成全部任务,不会提前结束了。
2.基于度量分析的程序结构
类图
这次作业在第二作业的基础上只是调度器类做了较大改动,大体框架未变。
SS Multielevator Complexity Metrcis
由上图可见,指令拆分的中间楼层的计算较为复杂,以及对于当前请求优先电梯的计算也较为复杂。其实只是判断条件比较多而已,并未觉得多复杂啊……
SS Multielevator Dependency Metrcis
由上图可见,我的类之间的依赖关系是比较多的。我采用的设计模式是观察者模式,订阅者(多部电梯)会依赖观察者,观察者需要通知订阅者所以也要依赖它们。对于某些大佬的上图中Cyclic一列为0,不知道是怎么办到的。既然要实现多各类之间的信息交互,是怎么减少类之间的依赖关系的呢?望赐教!
SOLID 分析
SRP:逻辑上认为这几个类的功能是比较单一的。不过由上面度量结果图可以看出,调度器的代码规模大且控制分支多。
OCP:符合,如果再增加电梯的数目,只需对调度器类增加几个电梯的队列即可,相应方法的改动也很少。
LSP:未使用继承,无法体现
ISP:未使用接口,无法体现
DIP:不符合。
3.BUG分析
感觉这次线程单元的BUG还是主要出现在如何控制线程结束以及保证线程安全上。
强测中我制造了一个40条指令的输入,只hack到了一个人。能力有限,我也想要强大的评测机……
心得体会
其实我觉得我在多线程安全方面还是可以优化的。为了保险起见,我给很多方法都加了锁,其实这其中一定是有多余的情况。多个线程或方法之间的同步与互斥是需要自己考虑来化简的。
我感觉一个单元的时间对于我们完全掌握多线程的知识点是完全不够的。其实只要你胆子大,很少的线程知识是完全可以搞定这些作业的。就算你使用了多线程的知识,这三次的作业我们也只是对个别知识点使用的比较多。因为了解的不够多,知识掌握的不够全面,我们的实现方法变得很单一。一直有种一知半解的感觉。当然"师傅领进门,修行看个人”,这些知识体系的完备还是需要我们花大量的额外时间来学习的。
加油啦,自己千万不要浅尝辄止啊,不要做一只井底之蛙!共勉!