设计策略
第一次
架构模式采用生产者-消费者模式,输入线程作为生产者,电梯作为消费者,共享对象为由一个自定义的Floor对象组成的数组构成。Controller类只起告知电梯线程输入结束的作用。
整体上采用LOOK算法的调度策略,由于第一次作业的电梯没有容量限制,所以可以采取尽可能把乘客关在电梯里的办法。比如当前电梯在1楼,现在有1号乘客要从1楼到2楼,2号乘客要从15楼到2楼。最快的方法是在1楼接到1号乘客后,直奔15楼,即使中途到了2楼,也不让这个乘客下电梯,直到到15楼把2号乘客接到,再回到2楼,让1号乘客和2号乘客一起下电梯。这样可以节省一次在2楼开关门的时间。尽管在现实中会给1号乘客极其不好的乘电梯体验,但是这样的确是在满足规则的条件下最快的。
第二次
架构模式采用生产者-消费者模式,输入线程作为生产者,电梯作为消费者,共享对象为由一个自定义的RequestQueue对象。这里不再使用第一次作业的Floor对象数组作为共享对象的原因是强测限制了最大指令数为50条,即使挨个遍历请求队列里的请求也不会有大问题,并且会在锁的问题上产生问题。如果锁整个Floor数组,那么Floor数组就和单独的请求队列一样,没有存在的必要。如果锁单个Floor,那么锁住了某个Floor,另一个Floor的数据可能会发生改变,带来线程安全上的问题。
由于加入了负楼层,-1楼到-3楼,使得楼层变得不连续,这样使得在调度请求、电梯运行时都极不方便。因此,受到操作系统中虚拟地址与物理地址的启发,将低楼层到高楼层依次映射到虚拟楼层上。这样,在使用电梯请求的时候,使用者可以使用到连续的楼层,比如电梯上楼时只需要将虚拟楼层+1,无需考虑实际楼层是否处于-1层。封装了与PersonRequest类不同的MyRequest类,其中楼层既可以返回实际楼层(方便输出用),也可以返回虚拟楼层(方便计算用)。
整体上继续沿用第一次作业的LOOK算法的调度策略,但是与第一次作业不同,第二次作业的电梯有容量限制,所以乘客能下就尽可能先下,以免占据电梯空间让本来可以捎带的乘客捎带不了了。
第三次
第三次架构模式为Worker-Thread模式。输入线程充当客户Client,发出请求,Controller类充当工厂车间Channel,电梯充当工人Worker。
电梯调度方面,由于电梯出现不能直达的情况,必须要把一个请求拆分为多个请求。由于动态拆分过于复杂,这里的拆分采用静态的划分策略,即拆分策略与电梯当前的运行状态无关。我采取了Floyd算法,在各楼层之间求最短路径。
优化方面,由于第三次加入了乘客等待时间作为性能指标,所以不再采取LOOK算法,而是采用SSTF算法(本质上是贪心算法),最终也获得了比较不错的性能分。
第三次作业架构设计的可扩展性
设计模式中的SOLID原则,分别是单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。前辈们总结出来的,遵循五大原则可以使程序解决紧耦合,更加健壮。
单一责任原则
指的是一个类或者一个方法只做一件事。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化就可能抑制或者削弱这个类完成其他职责的能力。
建立了一个MyReQueuestFactory工厂类,传入一个官方的PersonRequest,就可以返回拆分请求后的一组请求。如果以后需要更改拆分策略,在这个工厂类里进行修改即可,无需改动其他部分。同时,电梯也不会参与请求的调度过程,如果要修改调度算法,直接在Controller类里修改即可。
开放封闭原则
对扩展开放,对修改关闭。意为一个类独立之后就不应该去修改它,而是以扩展的方式适应新需求。
第三次、第二次的电梯除了修改运行策略外,其他均重用了第一次电梯的方法,因此开放封闭原则还是做的比较好的。
里氏替换原则
所有基类出现的地方都可以用派生类替换而不会程序产生错误。子类可以扩展父类的功能,但不能改变父类原有的功能。
本单元作业并没有利用到继承,因此在此不讨论里氏替换原则。
接口隔离原则
类不应该依赖不需要的接口,知道越少越好。
本单元作业也没有利用到接口,因此在此不讨论接口隔离原则。
依赖倒置原则
指的是高级模块不应该依赖低级模块,而是依赖抽象。抽象不能依赖细节,细节要依赖抽象。
我在依赖倒置原则这方面做得不太好,因为调度器内高度依赖电梯的内部结构,包括电梯到底是使用
基于度量分析自己的程序结构
协作图
类图
第一次作业
第二次作业
第三次作业
分析自己程序的bug
自己在强测、互测中均未发现bug。
在第三次作业自己测试的过程中,发现了CPU超时的bug。原因在于我判断电梯进入wait状态的条件沿用了第二次作业的,写成了hasInput() && elevCar.isEmpty() && tashQueue.isEmpty(),这样的话,如果输入线程还没结束,但电梯已经做完了自己的事情的时候,就不会进入wait状态,而是一直在轮询,最终导致CPU超时。
分析自己发现别人程序bug所采用的策略
本单元做得不太好,没有手写评测机(摸了)。一部分原因是自己确实比较忙,另一部分原因是多线程的评测机确实比较难写。因此策略一是采取读别人的代码,然后针对性的hack。而是构造边界数据,如在临界时间加请求,测试超载等。
心得体会
本单元游戏体验比较好,感谢评论区各位ju佬提供的各种优化策略以及各位助教给的宽松的性能分机制。
在多线程的测试里面,一次测试正确并不能说明程序对于这个测试用例是正确的,有可能同一个测试样例运行999次都是正确的,但是评测的时候那1次运行错误,导致WA。对于这个问题,没什么好的解决办法,只能是尽可能的多测试,尽可能减少错误发生的概率。