一、 前言
OO的复杂程度可能和写完的时间成正比,多项式时周三电工实习不带电脑,到了出租车已经发展成周三下午5点开始写Readme……不过相比前几次为语法耗费时间,出租车更多的是为设计来投入精力。多线程debug也很有意思,在写多部电梯时,自己猛然发现调试时加入的一句System.out.println竟然会对结果产生影响(事后才发现原来是输出调试信息到控制台耗费了时间,导致输出结束时其他线程的状态信息更新完成了,若不加输出,其他线程更新尚未完成,发生错误),真的迷……
二、 多线程电梯
由于三部电梯的出现,想要让他们在三个线程中自己完成调度,就不得不在一个大请求队列的基础上,增加三个小的请求队列,分别存储三部电梯的请求。
每当InputHandler有了新的合法请求,就拿到大队列的锁,将其加入到大队列中,叫醒调度器。同样的,每当电梯跑完当前主请求,也会叫醒调度器。调度器被叫醒后,要把当前大队列中的指令分给空电梯或能捎带的电梯,直至InputHandler线程结束且四个队列都为空,程序结束。
这次虽然要求使用继承,但由于有捎带的但电梯写的并不好,索性直接重写Scheduler类。值得一提的是时间的处理。但电梯由于使用假时间,导致时间的计算冗余重复。本次是使用系统时间,相应的就带来了误差,如由于程序运行导致的每层楼实际并不是3000ms,实际上会多出几十毫秒。误差的累计就导致了输入较多时,开始出现明显的时间误差。因此我使用了消除误差的方法,即在wait(3000)前,先计算所产生的误差时间,将睡眠时间改为wait(3000-误差),就使得时间完全满足要求。
与之前的问题大致相同,圈复杂度和嵌套深度过大。出现问题的carry方法是用来判断是否可以捎带,代码如下。
1 /** 2 * 查找是否可被捎带 3 * @param request 4 * @return 捎带返回true,否则返回false 5 */ 6 public boolean carry(Request request) { 8 if (request.get_flag() == 2) {// ER 9 if (eleList[request.get_ele() - 1].get_status() == EleStatus.RUNNING) { 10 if (eleList[request.get_ele() - 1].get_dir() == EleDir.UP && request.get_dstfloor() > eleList[request.get_ele() - 1].get_curflo()) { 11 move(request, eleList[request.get_ele() - 1]); 13 return true; 14 } 15 else if (eleList[request.get_ele() - 1].get_dir() == EleDir.DOWN && request.get_dstfloor() < eleList[request.get_ele() - 1].get_curflo()) { 16 move(request, eleList[request.get_ele() - 1]); 17 return true; 18 } 19 } 20 return false; 21 } 22 else {// FR 24 int whichtocarry = 3, minsumdis = -1; 25 for (int j = 0; j < 3; j++) { 26 if (eleList[j].get_status() == EleStatus.RUNNING) { 28 if (eleList[j].get_dir() == EleDir.UP && request.get_dir() == 1 && request.get_dstfloor() > eleList[j].get_curflo() && request.get_dstfloor() <= eleList[j].get_dstflo()) {// bugwhere 29 if (minsumdis == -1 || eleList[j].get_sumdis() < minsumdis) { 30 minsumdis = eleList[j].get_sumdis(); 31 whichtocarry = j; 33 } 34 } 35 else if (eleList[j].get_dir() == EleDir.DOWN && request.get_dir() == 2 && request.get_dstfloor() < eleList[j].get_curflo() && request.get_dstfloor() >= eleList[j].get_dstflo()) { 36 if (minsumdis == -1 || eleList[j].get_sumdis() < minsumdis) { 37 minsumdis = eleList[j].get_sumdis(); 38 whichtocarry = j; 39 } 40 } 41 } 42 } 43 if (whichtocarry != 3) { 45 move(request, eleList[whichtocarry]); 46 return true; 47 } 48 return false; 49 } 50 }
不好的一点是没用枚举而是以变量来表示状态,但由于捎带判断条件比较复杂,可能也很难优化了。另一个圈复杂度高的方法是opendoor方法,用来输出,同样的,因为加入了时间的误差消除,使得方法更加复杂,代码就不放了。
bug方面,自己没有被报bug,但是测试的程序问题较多。如捎带无法判断、时间计算错误等问题,可能是他没来得及调试导致的。
三、 IFTTT
本次作业是一个IFTTT的文件扫描。由于之前使用过类似的IFTTT类型app和坚果云这样的支持增量更新的云盘,所以我对这个思想并不陌生。不过由于自己的拖沓,导致了测试接口是在ddl前半小时写的,完全没有调试,在被测时还有点担心。
本次由于要扫描文件,而文件的位置(路径)实际上包含在了文件的名字中,因此没有用到树来存储,而是使用HashMap<String, long>来存储,以文件的路径作为key,以其大小和最后修改时间作为value,对目录进行snopshot。以一定时间间隔更新snopshot,对监控文件进行查找和比对,如果触发则进行任务,否则继续扫描。
我采用的是每条指令一个线程。在更新snopshot后,每条指令开始对其监控文件进行查找。因此就出现了一个问题:多条指令监控一个文件,有recover、detail、summary操作时,若recover先完成,很可能导致后两者没有被触发而未记录;recover后完成,很可能导致文件变化两次都被detail和summary捕捉到,记录了两次。这个问题是由于多线程的不确定性导致的,因此不算做bug。但当互测时还是被申报了,而且申报者说他解决这这个问题,我猜应该是将recover先不执行,保证一定触发两次。
可以看到,Monitor类很可能成为了一个god类,其中的snopshot方法和compareFile方法完全可以拿出来作为新的类。度量分析也印证了这个猜想:
如果将这两个方法拿出来,情况会好很多。这是在设计上没有想好就直接写代码的结果。compareFile方法复杂的原因,很大程度上是由于其中分了四种触发器,拆成四个函数会大大降低复杂度。
这次作业由于分类树设置的不好,导致同源错误很可能被多次挂树,如我的modified判断条条件多此一举的判断了大小,导致树上的detail、summary、监控文件、监控目录都挂了红……最后通过申诉解决了。我测试的代码同样出现了这个问题。除此之外,我已经多次通过读手中最核心的运算部分的代码,找到了难以测试的bug,也反映出了读代码的好处。
四、 出租车
出租车即使给了写好的UI和map,也没有帮助测试……由于100辆车每200毫秒运动一次,非常难观察其运动的正确与否,很多时候当看到GUI能正常运行,就基本判断没什么bug了,因此读代码的重要性更加明显。
本次我才用了100个出租车1个线程的假多线程,InputHandler线程输入后,将请求发给Scheduler线程。Snopshot则将每200ms的位置快照发送给调度器。调度器根据位置搜索派单车辆,更新该车辆状态,而TaxiList则每200毫秒sleep一次,再让所有车移动,然后叫醒调度器进行调度。
可以看到,许多方法出现了复杂度高的情况。由于车辆状态较多,到知道move等方法的复杂。
我测试的代码,出现了一个问题。通过读他的代码发现,他在判断派单车辆时,将信用大于当前队列最优的车加入队列,此后从中选择距离最小的一个,而忘记把信用较小的删除。同时,我的代码中也有个问题。快照对于每个位置的车辆,要用arraylist来存储,否则用int存其车辆编号,会导致同一位置有多辆车时,实际只存了1辆。
五、 心得体会
这三次多线程作业,让我从完全不会多线程的使用,到对锁有了一定的理解,收获很大。但是熬夜更加严重了emmm……每周三还要赶一天工才能将将完成。活着真好……