OO第二单元总结

OO第二单元总结

一、设计策略分析

1.第一次作业

  第一次作业应该是我花费时间最长的一次,由于第一次接触多线程,对锁的概念还是不能很好的理解,我首先花了两天时间去理解synchronized关键字,然后又用了一天时间进行架构的构思,从周三上机的代码中获得了灵感。直到星期五才进行代码的编写。想明白之后,思路也清晰了很多,最终要实现生产者和消费者对共享对象的互斥访问。

  第一次作业使用了消费者-生产者模式,生产者是外部请求的输入,消费者是电梯。它们作为两个线程共享一个请求队列,该队列放置于调度器中。电梯采用ALS调度策略。调度器中有用synchronized关键字修饰的put方法,Emptyget方法,和get方法。put方法由电梯输入类调用。Emptyget方法用于当电梯为空时,获取主请求。get方法用于电梯非空时,获取可稍带请求。其中Emptyget方法需要在调度器队列为空时,进行wait,然后由put方法notify唤醒,而get方法不需要wait。

 

2.第二次作业

  第二次作业在设计时延续了第一次的架构,所有电梯共享调度器的全局队列,而每个电梯内部又有自己的请求队列,通过Emptyget和get方法来从调度器获取请求。

  第二次作业添加的代码比较少。其中,电梯类新增peopleNum函数,用电梯内乘客个数加上未进入的请求个数来计算人数。多部电梯的情况下,所有电梯将会竞争获取请求,先拿到锁将获得优先选择权。与第一次电梯同样,电梯运行由电梯自己根据内部请求队列进行调度。

 

3.第三次作业

  第三次作业考虑到每种电梯的运行速度和可停靠楼层不同,如果依旧采用电梯竞争请求的方式,有些电梯可能长时间内负载都会比较小。所以我打算将调度器也作为线程,根据所有电梯的运行情况,将每个请求进行合适的分配。考虑到A电梯运行范围过大,分配时将最后考虑,而C电梯可到达的多数楼层,B也可到达,因此将请求优先分配给C电梯,其次是B电梯,最后是A电梯。我首先将第二次作业进行了一定的重构,然后考虑最重要的部分,就是调度策略。

  调度策略。对于每个请求,分配给电梯的优先顺序为C,B,A。对于可直达的请求,则直接按照分配的优先级进行分配。对于不可直达的请求,规定换乘站为1,5,15三层,随后计算在这三层中哪一层换乘所走路程最少。我考虑的是如果对于每个请求都计算最短路径,换乘的楼层都不统一,对于大量随机请求来说,会随机在各层开门,在不同楼层多次开门对效率的影响不能忽略不计。如果采用集中换乘,将减少楼层停靠的随机性,我只是出于一种猜测,并没有实际验证哪种方法在大多数情况下更优。出于简单易行的考虑,我还是采取集中换乘的做法。

  还有对电梯中人数的计算进行了优化,由于每个乘客都有对应的楼层区间,所以将电梯内所有未达到请求和乘客的楼层区间合并,只要判断在所有楼层区间内乘客数最大的是否超载即可。

 

二、可扩展性

  在功能上,电梯只需要负责根据内部请求队列进行运行,输入类需要将输入传入调度器,调度器按照电梯的运行状况将请求分配给合适的电梯。电梯和输入类的功能比较单一,提出新要求时,不需要做太多的改进。在性能上,调度器由于采用集中换乘,需要根据实际情况修改换乘楼层和分配的优先级。

  SOLID设计原则:

  SRP:电梯和输入类具有单一职责,而调度器需要主动获取电梯的信息,然后对电梯分派请求,违反SRP

  OCP:基本满足开闭原则。电梯和输入类每次都基本没做修改,调度器每次需要增加新的函数

  LSP:没有使用到继承

  ISP:没有使用接口。

  DIP:本次所用三个模块是并行的。我感觉需要在调度器和电梯中间加一层接口,以实现状态的获取和请求的分派。

三、分析程序结构

 1.第一次作业

  

 

 

 其中RequestInput类中只用了1个属性,Elevator使用了4个属性,不知道为什么CSA(attributes)计算差别如此之大.

  

 

 

 第一次作业比较简单,因此方法规模和控制语句数量都比较少的。

经典OO度量:

 

 

 

UML类图:

 

 

 第一次作业相对简单一些,只涉及一部电梯的运行,每个类与方法的复杂度都不会太高。调度器类作为共享对象,实现了电梯请求输入类和电梯类对调度器的互斥访问,职责分明。但缺点是只使用生产者-消费者模式仍然有一定的局限性,对于多部电梯的情况,调度器需要根据所有电梯的运行状况来分派合适的请求。

2.第二次作业

 与第一次作业相比,总代码量增加大约在40行左右,主要集中在Elevator,Controller两个类中。在Elevator中增加了peopleNum函数判断电梯人数,由于出现负的层数,所以还改变了up,down了两个方法。

 

 在每个类中的控制语句并不算太多,各个方法的行数也比较均衡,沿用第一次作业的结构,相对比较好的满足开闭原则,只进行了一些方法的增加。

UML类图:

 

 

 

 

Type Name NOF NOPF NOM NOPM LOC WMC NC DIT LCOM FANIN FANOUT
Controller 4 0 9 0 109 22 0 0 0.222222 0 0
Elevator 11 0 17 1 190 43 0 0 0 0 0
Main 0 0 1 1 15 2 0 0 -1 0 0
RequestInput 2 0 3 1 32 6 0 0 0 0 0

可以看出这些度量与第一次相比并没有太大的变化。事实上,第二次作业是我用时最少的一次,并没有花费太多精力。

 

3.第三次作业

  我首先对第二次作业作了重构,将调度器作为线程,实时参与接受请求和分派请求。利用第二次bug修复的预览,确认可以通过第二次强测的基础上,开始第三次作业的构思。

   其实我可以直接在第二次作业的基础上增加第三次作业的需求,不过我想尝试一下课上提到的观察者模式以及使用current包提供的lock锁。

 

 

 

 

UML类图:

 

 

 

 

 

Type Name NOF NOPF NOM NOPM LOC WMC NC DIT LCOM FANIN FANOUT
Controller 10 0 15 1 288 69 0 0 0.133333 0 0
Elevator 18 0 32 2 354 82 0 0 0.0625 0 0
Factory 0 0 1 0 27 4 0 0 -1 0 0
Main 0 0 1 1 16 1 0 0 -1 0 0
RequestInput 2 0 2 1 28 5 0 0 0 0 0

  进行重构之后,调度器和电梯都作了一部分变化。原来电梯的Emptyget方法和get方法被删除,换为调用controller的telled()方法,将controller从wait中唤醒,以进行请求的分配,电梯还可以RequestInput类唤醒,只有在满足这两个条件的之后才会进行请求的分派。电梯在调度器中使用Treeset存储,电梯实现了Comparable接口,在集合中按照类型为C,B,A的顺序存储。每次分配,将遍历电梯集合,按照电梯现有最大人数和电梯与当前请求出发点的距离,按照最大载人数的权重计算每部电梯的得分,请求将分配给得分最小者。而率先分配捎带请求,否则当有大量请求从楼房底部到达顶部,所有电梯都将运动到顶部,这样对于之后出现于底部的请求不利。若无法分配为捎带请求,随后则将进行主请求的分配。若分配失败,则不做相应处理,等待下一轮的分配。

  第三次作业的改变与前两次相比,不是单一的生产者-消费者模式,电梯和调度器还有请求输入类都采用了通知的方式来实现同步。电梯每到达一层将会发出信号,调度器每次设置主请求将会对电梯发出信号,将其从等待状态激活。请求输入类发出请求时将会对调度器发出信号,使其进入等待电梯信号的阶段继续等待。

  而且当多部电梯提出接受请求时,调度器不会像前两次作业那样优先将请求分配给率先提出请求的电梯,而是按照当前运行状态分配得分最低的电梯,这样可以在一定程度上实现负载均衡,提高每部电梯的利用率来提高整体的运行效率。而且调度器中的队列将优先尝试分配先来的请求,可以使乘客的等待时间尽量缩短。这样比较简单的实现,获取不低的性能分。

 

四、程序的bug

  这三次作业在评测和自测时均未发生死锁问题。

  第一次作业并没有发现bug

  第二次作业出现了致命的失误,我错误地把最大载客量错写为8,导致有5个强测点超载,险些没有获得互测资格。

  第三次作业强测比较顺利,但是互测被3个人找出了一个RTLE。奇怪的是他们的数据几乎一模一样,都是在某一时刻有多个人从-3楼到14楼。原来是我的捎带请求判断出现了错误,由于到达-3层时电梯运行方向仍然为向下,按照我当时的逻辑是判断该请求和电梯运行方向是否一致。因为我是在关门后才改变运行方向,所以其他人永远都不会被捎带,每次电梯中只会有一个人。导致了超时问题。修复该bug后,发现强测两个得分为80的点,所需时间比原来少了很多。我真的后悔,当时思考不够严谨,没有多做测试,造成了性能分的严重损失。

 

五、发现别人bug

  这三次作业均未找到别人的bug。我是尝试增大请求的间隔,测试有没有人出现CTLE的情况,但是没有成功。

  线程安全相关的问题,感觉大家都处理的比较好,大多数人都是生产者-消费者模式,实现对共享对象的互斥访问。

  本单元的测试策略主要是看是否超载,是否RTLE,是否CTLE,还有线程安全的问题,第一单元不同的人可能犯错的地方有很多,尽量使用比较随机的测试样例去测试,第二单元中随机性更大,单纯使用随机样例难以找到错误。

 

六、心得体会

  第一次接触多线程,经历了三次作业之后,对线程安全,我们要做的是确保共享对象在一个时刻只能被一个线程访问,围绕这一点,利用生产者-消费者模式,在共享对象中实现不同线程对该对象的需求和访问,通过synchronized关键字实现对共享对象的保护。在第三次作业中,尝试了concurrent包的ReentrantLock和Condition,实现了请求输入类和调度器之间的通信,调度器和电梯之间的通信。调度器被唤醒后根据电梯的整体运行情况将合适请求分配给电梯。

  电梯的换乘请求使用Hashtable存储,key为前一部分请求,value为后一部分请求,完成前一部分后,查找Hashtable若存在则将后一部分发送给调度器,并将其从Hashtable中删除

  还有整个程序的终止条件的设计,在请求输入类结束输入后,将调度器的标志位置false,若标志位为false,并且换乘请求的Hashtable和请求队列均为空时,向所有电梯发送notify将全部唤醒,待它们结束所有请求后,判断标志位为false,将自动退出。

  最后,还可以作的改进是,在调度器和电梯间加一个接口,调度器通过接口获取电梯信息,并分配请求,电梯在完成换乘请求前一部分后,通过接口传递给调度器。减少调度器和电梯之间的耦合。

 

posted @ 2020-04-18 21:13  困于街头  阅读(167)  评论(0)    收藏  举报