BUAA_OO_2022 Unit2 总结
BUAA_OO_2022 Unit2 总结
一、第五次作业
1、同步块的设置和锁的选择
这是第一次写多线程的程序,所以对此还比较陌生,于是向上机代码学习了一波,并偷懒躲开了一些问题。线程分为了输入线程,电梯线程和调度器线程。在我的第一次作业中,输入线程和调度器线程共享一个总的请求队列,调度器和五部电梯共享着5个请求队列,因为第一次作业时对同步块和锁还不是很理解,所以就暴力的为共享对象的所有方法加上了锁,也正是因为对同步块的陌生,我令每部电梯都将请求取出自己存放,以此来尽可能的规避冲突,并为下一次的作业增加了工作量。
不过尽管我尽量让线程各自独立,依然出现了问题——输出时的线程不安全,时间戳没有严格递增。虽然指导书中有说需要自行保证输出包被多个线程调用时输出符合逻辑,但是我当时对多线程的浅薄理解不足以让我理解调用输出包会出现何种问题,直到后来看到讨论区的帖子,才理解到对于输出这一过程,可能存在A获取时间戳然后B获取时间戳并输出,A最后再输出这样的情况,只有将这一过程原子化,才能保证在一次输出过程中不会被另一个输出临时插队。
2、调度策略
在第五次作业中,调度器没有起到任何实质性的作用,只是从总请求队列中获取请求并添加到相应楼座的请求队列里。对于请求的处理完全依赖于电梯本身,电梯会在开关门和运行的间隔查看共享队列中是否有请求,如果有就将将其取出并保存在自己的队列中,对于自己内部的请求则分为电梯上和等待中。在对请求的处理方面,采用能出就出能进就进的策略,而电梯的移动则是完全依赖一个获取方向的函数,在每一次开电梯门以及增加请求的时候,都会判断一次接下来运行方向,如果在电梯内的请求有需要继续前进或是前进方向有请求在等待,电梯就会继续向前运行,如果都不满足且有请求未处理,则会改变运行方向并按照相同的逻辑进行判断。这属于是将请求交给电梯并由其自行处理,在之后的作业中仍继续沿用。
3、UML类图
第五次作业里InputThread和Schedule共享一个总的请求队列,由InputThread接收输入并添加,Schedule取出并分发。Schedule同时和每个Elevator也共享着请求队列,用来实现请求的分配。
4、bug分析
本次作业中出现了两个bug,一个是线程不安全的问题,这个bug是由于对同步块和锁的陌生导致的,另一个bug是逻辑上的问题,我在获取电梯运行方向的时候,即使已经满载且内部乘客的目标楼层都在反方向,当运动方向有请求时(应当在电梯满载时忽略等待队列中的请求),运动方向就不会改变,但是到请求楼层时,没有人下也没有人上,就导致电梯可能会突破1~10层的限制,考虑不周加上测满载时为了省事没有多改变请求的目标楼层导致没有发现这个明显的bug,只能说测试一定要更加全面。
在hack他人时主要针对线程不安全进行hack,具体就是每隔一定时间同时投入大量请求以期最大可能遇到时间戳不递增的情况。
二、第六次作业
1、同步块的设置和锁的选择
第六次作业中,首先是移除了第一次作业多余的synchronized。然后就是我在第二次作业中选择一类电梯共享一个请求队列并对其进行访问,而不是每部电梯独自拥有着一个请求队列,所以我在处理进出乘客时锁住请求队列,但是在判断是否需要开门,开关门,获取电梯运行方向的时候不同步。这样的处理方式 使得同一楼座或者同一层的的电梯在面临大量请求时节省了部分时间。只锁请求队列是因为修改都发生在请求队列身上,所以只要锁住请求队列即可。还有就是在向请求队列中加入新的请求时也会锁住队列。
2、调度策略
在第六次作业中,调度器依然没有起到什么实质性的作用,依然只是在分配请求。我在最开始也考虑过是否要写均分,但是思考过后认为让电梯自行竞争请求是一个简单且高效的办法,只是需要小心冲突的发生。写调度策略如果简单均分虽然没有什么冲突,但是效率在不少情况都比不过自由竞争,而如果获取电梯状态,请求状态进行计算分配,虽然理论上比较快,但是容易出现问题,所以综合考虑后选择了自由竞争的策略。
3、UML类图
本次作业主要是增加的了横向电梯,并且每一座,每一层都可以拥有多部电梯。整体的框架没有改变,依然是由共享的请求队列分别联系着输入和调度器以及电梯和调度器。对于新增加的横向电梯,我将其方法写入了原本的电梯类中,通过isBuildingType()判断是否是横向电梯,在各种判断中利用这个函数选择调用哪种判断方式。因为电梯的类似,大部分的函数是可以共享的,减小了代码量。相比较第一次作业,我还将电梯线程的启动放入输入线程中,首先启动基本的电梯线程,之后在新增电梯时再新增线程,同时记录所有的线程以防需要删除电梯。
4、bug分析
在本次作业中,依然出现了电梯跑到不存在楼层的问题,这次的原因主要是因为我锁的范围比较小同时没有控制楼层的范围,而且在我的程序中,当进行完开关门后会进行一次移动。在最初我将判断开关门及一系列操作原子化,但是这样同一楼座的电梯在同一层会浪费比较多的时间,所以我最终只将进出乘客的部分同步,也因此会出现这样一种状况:A电梯正在第10层开门,此时没有上锁,且B电梯来到了这一层,B电梯可以访问请求队列发现有请求待处理于是开始开门,此过程中A电梯开始进出乘客并将请求队列清空,B电梯在A电梯释放锁后没有接到乘客,所以不会改变运行方向,但是因为B电梯进行了开关门,所以会有一次移动,从而会跑到第11层,我在修复时判断只有电梯内不空且等待队列不空才移动,修复了bug。这个bug只有在1或10层且不同电梯都进入处理过程中才会触发,比较难看出,所以在自行测试时没有发现。
在hack别人时主要是针对自己写代码时发现的bug,最主要的是一个线程对共享对象进行遍历时另一个线程对共享对象进行修改从而导致的错误。
三、第七次作业
1、同步块的设置和锁的选择
本次作业的改变是电梯可以定制并且请求可以不同座不同层。因为先前作业的bug,所以这次我选择由调度器分发请求来减少冲突发生的可能性,所以需要锁住的地方就是添加以及删除请求的位置,此外就没有什么需要同步的地方了。
2、调度策略
在第三次作业中,调度器终于发挥了作用(可喜可贺),所有的请求都由调度器进行精细的分配。本次作业中,对于复杂请求的路线规划是最难处理的,我最终采用的办法是直接指定横向电梯,因为在动态选择的过程中出现了一些bug。对于一个请求,它会有当前目的地和最终目的地,电梯的判断都是基于当前目的地并在送达后将请求投回主请求队列,如果未抵达最终位置,则由调度器再次计算当前目的地并分配出去,分配时优先考虑速度快乘客少的电梯。电梯则根据自身的请求队列,按照之前作业的逻辑运行。
3、UML类图及UML协作图
本次作业的改变是定制电梯以及复杂的乘客请求,虽然需要更多的判断,但是整体框架依然没有改变,直接在上一次作业的基础上进行进一步的开发便完成了作业。主要的不同之处在于本次作业中让调度器首次发挥了作用,并且因为电梯也会新增请求改变了线程的终止条件。总体上是只进行了增加,以及少量的修改。
4、bug分析
本次作业在互测中被多个新增电梯请求后接一个单请求刀中,检查代码并在本地测试数十遍后仍未复现问题且在再次提交后通过该数据,推测可能是上锁的范围小了。
在互测中发现有些同学对于电梯可达性的判断不够充分,只判断了可达目标楼座,忘记判断是否可达出发楼座。
四、hack策略及心得体会
我的hack策略主要是根据自己编写程序时遇到的问题构造数据以此来hack。但是我并不能考虑到所有的情况,忽略的情况才是最容易hack别人的数据,所以对于共性的隐蔽错误我难以发现,只能尝试别人是否有我之前的错误。手动评测还有一个问题是有的错误可能需要跑多次才能出现,自己测试跑过一遍OK就换另一种可能的数据了,用随机测试不停测试却能发现问题。
通过本单元的三次作业,我对多线程有了更深的认识,也对锁有了一定的了解,并深刻体会到了多线程并发时状态的多样性,对如何避免冲突也有了一些收获。在debug以及hack别人的过程中我认识到了对于这种有大量输出的程序而言自动评测的重要性,纯靠自己测试终究是有极限的。
虽然在本单元出现了一些愚蠢的错误,但是都只是需要修复而非彻底重构(可能也是因为模拟电梯的框架比较固定),后两次作业都是在先前作业的基础上完成的,虽然第二次因为第一次的偷懒改了不少地方,不过整体思路并没有改变,而是延续了下去,所以实际上并没有花太多时间。
总体而言这个单元确实是收获满满,电梯的调度是一个非常有意思的问题,但比较可惜的是最后一次动态规划路线时出锅了,为了保险最终还是直接敲死路线,还有就是下次一定要写自动测试,手动测试又痛苦又测不全,不能再重蹈覆辙了!