UnitTwoSummary

这篇博客主要介绍了作者在三次作业中设计的电梯调度策略,从单部电梯到多部电梯,并涉及换乘问题。作者分析了作业的设计思路,包括生产者-消费者模型的应用、LOOK调度策略和换乘策略。同时,对代码的可扩展性进行了基于Solid原则的评价,指出调度器的冗余问题。此外,还进行了度量分析,针对不同作业指出了代码的复杂度问题和解决方案。博客还涵盖了Bug分析和手动构造测试用例的重要性,以及作者对代码的反思和未来改进方向。
摘要由CSDN通过智能技术生成

目录

  • 一、作业设计策略
    • 第一次作业
    • 第二次作业
    • 第三次作业
  • 二、第三次作业架构设计可扩展性
  • 三、度量分析
  • 四、BUG分析
  • 五、Hack策略
  • 六、心得体会

一、作业设计策略

第一次作业

思路:
  • 第一次作业较为简单,只是单部的可捎带电梯。但由于笔者博客周摸鱼对多线程啥都不会,一度十分绝望视死如归, 代码架构分为三个部分:输入线程、调度器和电梯线程。应用的是典型的生产者-消费者模型,调度器作为输入线程和电梯线程的共享对象。调度策略方面,用的是教程中给出的ALS调度,在携带上改了一下,改为不管是不是同方向都携带上以减少开关门次数,节省时间。
  • 在电梯运行方面,利用大家都是"纸片人"的特性。电梯每到达一层康康这一层有没有操作,如果有的话,先开门0.4s,然后再刷新一遍这一层的操作请求队列(为的是利用开门的时间,捞上尽量多的请求)。然后上下人,最后直接关门走人。
  • 在结束电梯线程方面,在调度器中设置一个isend,拥有set和get方法(线程安全,记得锁上)当输入读到null后,set它。如果isend并且请求队列为空,就让电梯线程结束。
  • 注:线程安全,读写互斥 & 善用wait+notifyall避免暴力轮询

第二次作业

思路:
  • 第二次作业为多部可捎带电梯。有了第一次的基础,感觉实现上并不难,但是由于性能方面的考虑,笔者前前后后共写出了三版不同调度策略。虽然最后发现仿佛不比无为而治快到哪儿去 整体架构还是延续第一次作业:输入线程、调度器和多个电梯线程(电梯线程根据需要个数开启)。调度策略方面,采用的是LOOK调度,也就是说电梯每次完成一个方向的运行,尽量把这个方向上的所有任务都完成再掉头。而在电梯得任务上,采用的是事先根据各个电梯的状态将请求放入适合的电梯请求队列中,而非让电梯自己抢,这样可以在一定层面上避免一部电梯在某层抢了很多任务,而其他电梯啥都没抢到,没活儿干。
  • 具体电梯分配策略:每个电梯都拥有两个队列,requestlist(外部还没上电梯的等待队列,后面简称为rlist),innerlist(电梯内部已经上来了的请求队列,后面简称ilst)。每个电梯都拥有一个ElevatorStatus对象,对象被相应电梯线程和输入线程所共有。ElevatorStatus对象拥有direction、currentfloor、elevatorname、passagernumber的属性,拥有get和set方法。电梯线程实时更新status信息。
  • 调度器中拥有getNum()方法,用来获得如果电梯待处理的请求数量(更细致的说是如果新请求上了此电梯时,电梯还要处理的请求数量)。具体操作:先检查电梯ilist队列,如果电梯在向上走并且队列里请求的到达楼层>新请求r的起始楼层,则说明在r进入电梯时此请求还没有下电梯,电梯还得继续处理它,所以将num++,电梯向下走同理。再检查rlist队列,以电梯向上走为例,如果rlist队列里面的请求的起始楼层大于现在的楼层,并且这个请求也是向上走,并且在新请求进电梯时他还没有出去,说明这个请求在新请求进电梯时,也在电梯里,并且还没被处理完,因此将num++,向下走同理。
  • 调度器中还拥有getSize()方法,用以获得一个电梯rlist和ilist中请求数总和。
  • 总体思想:实时获得电梯状态,在获得一个新请求后,先查找能够顺路将它接上并且任务数最少的电梯,顺路是指比如电梯向上走,请求在电梯上面并且请求也是向上走的(查找任务数使用getNum()方法,其所需电梯状态从ElevatorStatus中获得),并将其塞进电梯的rlist里;如果没有,查找总任务数最少的电梯(查找总任务数使用getSize()方法,其所需电梯状态从ElevatorStatus中获得),并将其塞进电梯的rlist里。

第三次作业:

思路:
  • 第三次作业为多部可捎带可换乘电梯。在调度策略方面我还是沿用了第二次作业的LOOK调度。在换乘策略方面,采用的是能不换乘就不换乘,如果非得换乘,则尽量同向换乘,如果同向换不了,则选择反向走最少的路的换乘方案。现在看起来我的换乘策略显然有些过于果断,在一些特殊换乘时会增大工作量,感觉没有打表来的好。那为啥不打表?懒是原罪
  • 在电梯任务分配方面,沿用第二次作业的方法(可以保证在开门的时候浪费电梯里人的时间数最少)。但是对于换乘请求的后半部分,我在把它拆出来后即放入一个secondlist的大队列中,在其前半部分执行完后,再将其从secondlist中取出,通过第二次作业的分配策略,将其扔进相应的电梯队列里。

二、第三次作业架构设计可扩展性

基于Solid原则的评价:

SPR(Single Responsibility Principle)

对于输入线程和电梯线程的确做到了各司其职,输入将请求处理分类给到调度器,电梯每到一层都从调度器取请求。但是调度器有一些过于冗余,基本所有关于分配的事情都干了,导致一共300+行,可读性也不太好。

OCP(Open Close Principle)

类、函数对于扩展是开放的,对于修改是封闭的。除了调度策略外,其他的如电梯运行形式,输入的处理等,都是在上一次的基础上进行扩展。

LSP(Liskov Substitution Principle)

没有应用继承。

ISP(Interface Segregation Principle)

没有应用接口

DIP(Dependency Inversion Principle)

没有应用接口。

总结:在SPR和OCP方面,我的设计大部分是符合的,但是调度器线程干的事儿太多了,基本把好几个不相关的事情也聚合在了一起,这是非常不应该的。此外,我的第三次作业中没有使用继承和接口,在以后的作业中,我认为这是我在设计层面上需要进一步思考提升的地方。

三、度量分析

第一次作业:

度量分析:

可以看到电梯类在这次作业中复杂度爆了,细究其原因是run方法。我第一次作业使用的是ALS调度,因而要确定主请求,而我在主请求的确定时,太过于冗余,很多地方造成了重复,也导致while、if一层套一层,最后复杂度飙升。

类图:

UML协作图:

第二次作业:

度量分析:



可以看到本次作业中Dispatch和inputThread的复杂度较高。对于inputThread可以看出是由于run方法。具体的来讲,是因为我在run方法中进行了请求分配,但是run方法其实不应该干这么多的事情。他只是应该从输入得请求然后扔进调度器中的队列,而哪种请求扔进哪个队列,则应该另开方法或者类来做,而不是都让run做。此外,对于Dispatch,可以看出其getAll方法的复杂度比较高,细看发现这个方法在预估新请求进入电梯时,电梯里还未处理完的请求个数。这里我现在还没想到比较好的解决方法,唯一想到的就是预估的时候将电梯外请求和电梯内请求分开,降低复杂度。

类图:

UML协作图:

第三次作业:

度量分析:




可以看到,Dispatch的复杂度标高,原因是因为我这次Dispatch里面包含了分配请求这一动作(也就是用到了那个复杂度很高的getAll和handoutRequest方法),在这里还没有很好的解决办法,除非将分配请求的种类再细分,然后不同的方法负责不同的种类。此外,因为新增了换乘楼层的需求,我获取电梯每一层的待处理事件getexe和看电梯需不需要掉头的方法judgedirection的复杂度都增加了很多。除此外,input线程那里,分割换乘请求,我的复杂度有些高,后来仔细看并想了一下那些代码,发现其实上行请求的分割和下行请求的分割在一定程度上是可以合并的,如果合并了,将会大大减少复杂度。

类图:

UML协作图:

四、Bug分析

公测和互测Bug:

公测和互测中未被发现Bug

自己写代码时候的Bug:

结束线程的Bug:

第五次作业:我是设了一个isend标志,然后结束的时候如果isend并且请求队列为空,那么就结束。但是没结束的时候,我会去判请求队列是不是空,如果是空就wait住。就像下面这样。


 while (requestList.isEmpty()) {

     try {

         wait();

     } catch (InterruptedException e) {

         e.printStackTrace();

     }

 }

但是这又一个很致命的问题,就是如果你在跑完所有请求再ctrlD(将isend置位),电梯线程就有可能没办法停下来。因为那时候请求队列已经empty了,即使wait被唤醒,也跳不出while循环。

解决方法:在while循环中加一个特判:如果isend被置位并且请求队列空了,就强制退出循环,并返回循环结束的标志。

暴力轮询的Bug:

第七次作业:在while循环中,为了得到换乘请求后半部分的状态,调用了一个方法,然而此方法会在while循环中一直不停的被调用获取,导致cpu时间爆涨。

解决方法:善用wait和notifyall(或await+signalall)保险起见,可以在在while循环中干了任何事儿后,都打印一下能代表它发生了的特殊的标志,看看会不会停不下来。

五、Hack策略:

手动构造测试用例:

对前两次作业而言,我手动构造的测试用例是让电梯在一个时间或者一小段时间内突然进来一大堆人,如果对电梯进人处理不当的话,会造成超时。第二次作业还对可能超载的情况构造了数据。第三次作业中,我列全了所有换乘的可能,形成了一个大数据,成功一个数据hack住了两个人。
小数据、边缘数据很重要,在第三次作业中,我以一个1-FROM-3-2的数据成功hack住了一个人,而且他只在只有一条换乘命令时会出错。感觉是电梯启动没处理好的问题。由此我所犯过的错误和hack住其他人的错误可以看出,电梯的启动和结束时刻值得我们尤为注意!

自动化测评:

在此单元作业中,我同样造了一个自动评测机,python脚本产生随机数据、由于python相关知识欠缺,应用java程序实现正确性验证,通过shell脚本将它们连接起来。
shell脚本如下:

i=0
echo "">Result.txt
while[ $i le 100]
do
	python3 data.py>Data.txt
	cat Data.txt>$i.txt
	cat Data.txt|java -jar Code.jar>myRe.txt
	cat myRe.txt>Re$i.txt
	echo $i>>Result.txt
	java -jar check.jar>one.txt
	cat one.txt>>Result.txt
	echo $i
	cat one.txt
	echo -e "\n"
	let i++
done	

六、心得体会

反思自己代码

  • 首先,方法耦合度高,有些可以拆开的方法还是合在一起写了。虽然相比第一单元有所提升,但是面向对象的思想还是不够。
  • 其次,对于线程理解的不透彻。这几次作业里由于在线程方面没出现啥大问题,就死鱼安乐了,所以后来出现了暴力轮询的问题,也没有想到好的应用wait和notifyall的方法来解决,而是换了个位置剑走偏锋避免了这个问题。即使第二单元过去了,对线程的学习还是不能停啊。

一些想唠唠的话

  • 电梯单元是真的让我意识到了自动化评测的重要性,因为多线程的特殊性,即使是同一个样例每次运行都会出现不同的结果,结果是正确的还好,如果是错误的的话,手动输入数据的同学先不说能不能有足够的数据将可能会出现的这些错误测出来,就是出错了复现错误都将是一个体力活儿。Debug难度可想而知。而且在性能方面,也只有海量数据多次跑,才可以知晓自己的性能到第如何,做到心中有数。
  • 此外,一开始的线程框架至关重要。由于我一开始只开了输入线程和电梯线程,然后一个调度器对象,后面一直在此框架基础上迭代迭代迭代,不仅每次调度器都有大更改,第三次作业调度器的复杂度更是爆表。我有看到过一种框架是将调度策略和电梯运行彻底分离,优化策略单独开一个策略类,每次想换策略,只需要去改那个策略类,甚至还可以有多个策略类分别去运行看哪个性能高的操作,这些都值得我去学习。
  • 最后,虽然这三周过的倍感煎熬,但看到成绩总会觉得是值得的,感觉自己的水平也在不断的提升。为了不辜负助教老师们为我们铺设的学习环境,也为了不辜负自己,我一定会继续努力,投入百分百的精力进去,提高自己的编程能力!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值