2019-OO-第二单元总结
多线程电梯调度问题
思路综述
第一次作业
第一次作业是非常简单的傻瓜电梯,不需要考虑容量,不需要考虑调度策略,运用了基本的生产者消费者模型,而且生产者消费者模型也一直贯穿了三次作业。
第二次作业
按照指导书写了ALS调度方法,在elevator的run方法中,楼层每变化一次,都先判断电梯中是否有人(runlist),如果有人,则取第一个人为主请求,目的地即为这个人的tofloor,否则取电梯外的第一人(requestqueue),目的地即为这个人的fromfloor,根据得到的目的地判断电梯运行的方向(这里出现了问题,遗留到了第三次作业导致强测CPUt了QwQ)。然后在每层楼判断是否有目的地同方向的人,是否有要出去的人。最后电梯运行一层楼。
第三次作业
单部电梯的运行和第二次作业中基本一样, 除了限制了容量,在进人的时候需要加一个判断。本次作业的难点(对于不优化的我而言)应该是请求的拆分。请求的拆分,主要问题在于如何找到最短的拆分,和保持拆分后的时序,即一个请求的前半段拆分送达后,才能执行后半段拆分。对于前者我的实现是把三个电梯23层楼,类比成一个23*3的迷宫,然后用bfs来解决,与普通电梯不同的是,对于同一个电梯,即迷宫上的同一列,即使上下的两格标记为1(障碍,不可达),该电梯也是可以经过这一层的,只不过不可以在这一层换乘,故而判断条件有所变化。
上下移动:
左右移动:
对于后者,我创建了一个MyRequest类继承PersonRequest,多了一个state属性,当且仅当state为0的时候,这个请求可以被电梯接入,而当每个请求出电梯的时候,就遍历后面的请求,找到id一样的第一个请求,将它的state设为0。
代码分析
结构分析
第一次作业
整个程序都很简单,只有elevator的run方法数据偏高。
第二次作业
和第一次作业一样,程序思路比较简单,没有很复杂的方法,elevator的run方法数据偏高。
第三次作业
可以看到bfs方法的结构化程度、循环复杂度都遥遥领先,而Bfs类的类方法的平均循环复杂度也当仁不让地排在了第一,电梯线程类和调度器类的方法总循环复杂度也非常高。
bug分析
前两次作业没有什么bug,只不过第二次作业的优化性能分较低。第三次作业出现了CPU tle的问题,检查了很久很久,一直在while循环体里找,看是否发生了轮询,经过了漫长的printf大法之后,发现是一个惊天大bug,这样看来强测只炸两个点都是我欧。问题出现在Schedule类中的get方法,在原来的get方法中,我遍历整个队列,如果找到符合要求的,则返回该请求,找到null,则返回null,否则wait循环,但是wait被唤醒过后,我返回了requestqueue.get(0)……这简直毫无道理了,即会导致电梯跑到它不该到的楼层一直等待。
为解决这个问题,我把get方法分离出一部分即getresult方法,在这个方法里,如果符合要求,就返回符合要求的request的索引+1,null则返回-1,否则返回0,在get方法中,如果getresult方法的返回值为0,则等待,被notify后重新调用getresult方法,while 返回值为0,则一直循环,直到返回null或者符合要求的request。
风格分析
比较喜欢我的第二次作业的作业风格,在elevator类中,每一个方法都比较短,都有自己独立的功能,结构层次比较清晰,容易理解。而第三次作业就存在了哪里有问题给哪里加补丁的情况,显得非常丑陋,甚至还曾经把bfs的迷宫放在scheduler类里,不过最后还是单独为它构建了一个类。
Hack Hack Hack
这次互测还挺不爽的,这老哥简直不顾OS期中考试,仿佛定了闹钟一样精准提交测试样例,然而到最后一波修复就解决了……bug列表 从来没有这么长过。
当然我们在跑评测机的时候,很少会去研究对方的代码,检查是不是同质错误,但我觉得提交二三十次,狂刀别人的做法还是没有什么必要,不过的确监督了我找bug。
难点总结
线程安全
在这次作业中,出了很多线程不安全的bug,在debug的过程中才逐渐对线程安全和线程不安全有了一些认识。比如说经常出现的一个问题:IndexOutOfBoundsException异常,这是因为Arraylist是线程不安全的,而我又没有锁成功。
最后感谢晶巨分享的一篇文章,
我们观察发生越界时的数组下标,分别为10、15、22、33、49和73。结合前面讲的数组自动机制,数组初始长度为10,第一次扩容为15=10+10/2,第二次扩容22=15+15/2,第三次扩容33=22+22/2...以此类推,我们不难发现,越界异常都发生在数组扩容之时。
由此给了我想法,我猜想是,由于没有该方法没有同步,导致出现这样一种现象,用第一次异常,即下标为15时的异常举例。当集合中已经添加了14个元素时,一个线程率先进入add()方法,在执行ensureCapacityInternal(size + 1)时,发现还可以添加一个元素,故数组没有扩容,但随后该线程被阻塞在此处。接着另一线程进入add()方法,执行ensureCapacityInternal(size + 1),由于前一个线程并没有添加元素,故size依然为14,依然不需要扩容,所以该线程就开始添加元素,使得size++,变为15,数组已经满了。而刚刚阻塞在elementData[size++] = e;语句之前的线程开始执行,它要在集合中添加第16个元素,而数组容量只有15个,所以就发生了数组下标越界异常!
来源于https://blog.csdn.net/u010010428/article/details/51258783
请求拆分
请求拆分的方法有很多种,先后考虑了地铁线换乘的模型(上学期数据结构作业题),即用Dijkstra最短路径算法,和BFS迷宫模型两种方法。后来还是选择了BFS迷宫模型,写起来比较容易。
wait/notifyAll
在我写第三次作业之前都没有搞清楚notifyAll和wait的作用,加了很多notifyAll避免线程一睡不醒,但实际上并不需要,总的来说,scheduler的get方法里需要用wait,如果电梯暂时没有可以执行的请求,且没有读取到null的时候,就让它wait,在add request的时候要加notifyAll,唤醒wait的线程。
感想
总的来说感觉不是很理想,出现了面对中测编程的情况,没有主动去编一个评测机,以致于自己的bug也没有好好找,在hack别人的时候也显得很无力(尤其是被别人hack几十次想反击一波的时候),还有关于线程安全、死锁等问题也没有仔细思考,而是等问题出了再去找寻解决方案。相比第一单元而言,作业变复杂,而自己反而变得有些松懈,不太ok。