buaa_oo_第二单元总结

 

 

第一次作业

 

 

架构模式:

  第一次作业只涉及每个楼座的一部电梯,不涉及横向电梯及换乘等,逻辑较简单。在架构上建立了Inputhandler类来处理输入请求,将请求加入同步队列waitQuene中;调度器设计:在课程组的强调和往届经验指导下,加入了Schedule控制器线程类,Schedule与Inputhandler共享同步队列waitlist,采用生产者-消费者模型,Inputhandler作为生产者,Schedule作为消费者。Schedule执行分配任务运行时从waitQuene取出一个等待者Person,由于本次任务较简单,直接加入对应楼座电梯等待队列即可。电梯类线程与Schedule共享各楼座等待队列,自身属性还包括电梯内队列,运行时首先是开关门逻辑判断,执行完开关门操作后为look策略的上下楼判断,电梯类中包含对各楼座等待队列WaitPerson的读和写同步块。我认为本次设计最为巧妙的是参考实验的PersonQuene类设计,将同步块变为类内的同步算法,在其他线程中编写代码时减少了很多考虑,添加同步块的区段,只需要调用同步方法即可,非常方便。

同步块和锁的设计:

  在架构设计中已经提到参考实验做法将许多同步方法如add,take,setEnd,isEmpty等封装进了PersonQuene类中,在其他线程使用时直接调用即可,无需过多考虑,非常方便。Inputhandler和Schedule共享总等待队列WaitQuene,Inputhandler对该队列操作只有add,Schedule对该队列操作只有take,直接调用封装的同步方法即可。Schedule和Elevator共享楼座等待队列PersonWait,Schedule对其的操作只有add,调用同步方法即可,Elevator对其的操作为遍历循环两次,在有人要上电梯时将其从队列中移除,该移除操作包括在循环操作中,故需要添加两个锁为该楼座等待队列的同步块。

未来扩展能力:

  虽然第一次作业可以不使用调度器类,为了后续作业的可扩展性,我还是加入了调度器类,心想后续几次作业中对不同电梯进行调度时,调度器类的分配算法可以从直接分配至对应楼座改成平均分配或按最短路径分配。其余类如Person,elevator都有不错的可扩展性,基本囊括了大部分主要功能,后续作业只需要增加一些属性方法即可。横向电梯也只用在普通电梯基础上改进即可。

分析出现的Bug:

  在程序编写时,由于对同步操作理解不透彻,对wait,notify方法不甚了解,中测中就出现了ctle,rtle等各种错误,认真分析程序流程,在相应的代码加入wait和notify方法后分别解决了rtle和ctle问题,加深了对同步方法的理解。强测中没有出现问题,互测中发现同步块设计不合理,提交的内容并没有给循环遍历操作上锁,造成了遍历时队列不同步问题,发现和解决该问题也加深了共享对象和对同步块的理解。

  互测中出现的bug还有线程输出不安全问题,一共被hack了9个点,强测估计特意没有考虑这个错误点,不然以输出不安全的情况下肯定是无法通过的。

第二次作业

 

 

(FloorElevatorQuene和ElevatorQuene为废弃但未删除类,其实只相当于Arraylist)

 

 

架构模式:

  第二次作业中增添了横向电梯和同楼座的多部电梯,整体架构上继承了第一次作业,还是由Inputhandler线程接受输入,增添了新增纵向电梯和横向电梯的功能,将请求加入同步队列waitQuene中。调度器承担分配功能(具体策略见下文)。本次作业带来麻烦的有两点,一是调度器的设计,二是横向电梯及横向算法的设计。在参考评论区同学提议下,横向电梯编写几乎完全参考纵向电梯,将左右对应上下,look算法则默认现在处于0-4层中的第2层,如本次在C座,则D座3楼,E座4楼,A座0楼,B座1楼,这样可以几乎不需要改变纵向电梯类方法的情况下完成横向电梯类的编写,只需要在横向电梯类增添一个将楼座映射为虚拟楼层的方法即可

 

调度器设计

   本来打算使用平均分配或是分配给最近电梯+容量特判,但是通过阅览往届博客和评论区分享,发现大部分采用了自由竞争的方法,我思索了一会,发现我的架构实现自由竞争非常容易,第一次作业中我的电梯控制内外两个队列,外队列是整个楼座的等待队列,如果采用自由竞争的方法,调度器的调度策略几乎不变,判断请求的楼座并加入对应楼座的等待队列即可,同楼座电梯共享该楼座等待队列,读取队列内容,通过同步代码块的方式,控制对该队列的写操作只能同时由一个线程执行,即乘客从等待队列中离开,只能上一部电梯,不会出现一个乘客上多部电梯,或是有乘客得不到运送这样的情况。整体仍遵循生产者-消费者模型,调度器和电梯共享各个楼座各个楼层的等待队列,调度器向其中写--加人,电梯读-遍历循环,写-出电梯。

同步块和锁的设计:

  延续第一次作业的设计,将同步方法封装至PersonQuene中,在线程中直接调用,保证对共享对象的同步执行。其余同步代码块也与第一次类似,但并没有仔细分析这次作业同楼座有多部电梯,他们的wait-notify逻辑是否有问题,会不会出现轮询的问题,而是自负地认为程序没有问题,这导致了强测中出现了致命bug(分析见下文,简单来说是同楼座的两部电梯只有一部在运行一部在wait时,运行的电梯不断notify另一部电梯出wait状态,二者一前一后进入wait态也会互相唤醒停不下来轮询ctle)。这个bug的来源包括了参考第一次实验代码中,封装的同步方法有很多notify,当时理解什么用法,默认是对的,并在同步代码块中参照模仿,每一个sychnoized都配一个notify,导致wait-notify逻辑有问题。

代码扩展能力:

  首先分析第二次作业在第一次作业上的迭代,构思本次作业时,发现要添加的点只有调度器的调度策略以及横向电梯类的实现,以及一些边边角角类的属性和方法添加。在迭代过程中在代码上花费的时间并不多,更多的其实是构思调度器调度策略和横向电梯look算法如何实现。最终在讨论区及往届博客提醒下采用自由竞争和完全参考纵向电梯实现横向电梯,迭代所改动的内容就更少了,调度器策略完全无需改动,电梯类内主要策略也无变化,改动几乎只在new方法中给各类实例添加新的属性而已。

  对于第三次作业如何在本次作业上迭代,当时想的是,如果没有送达至具体目标,就把该乘客重新加入调度器的总等待队列,调度器接着将其分配即可,可以说也非常简单,只需要在乘客出电梯时加一句条件判断和加入总等待队列即可。但后来仔细想想在原本架构上这样不知道如何判断各线程是否结束,多段路径如何分清乘客的from,to元素也存在问题,整个调度器可能也需要大改。

性能优化:

  本次作业花时间最多的其实是性能优化及与性能优化相关的debug操作,第一次作业据说ALS能拿98,look能拿更高,我的look一开始拿了94,这令我非常疑惑又很酸,明明策略都是一样的,第一次作业的逻辑也简单,为何如此差距,后来发现捎带策略有问题,向上运输时可能会把向下的乘客也带上。为了修复这一点点问题,居然花费了很久的时间进行特判,期间由于条件表达式的构造,书写问题出了各种各样的bug,虽然当时非常烦恼,后来想想这也使我更深层次思考了电梯的运行逻辑,同时为互测中hack数据提供了思路

分析出现的Bug:

  上文提到了我对wait-notify的设计逻辑有问题,导致了同楼座的多部电梯可能不能符合逻辑的进入wait阻塞态,具体可表现为一部电梯运行,另一部电梯wait时,运行电梯会调用同步代码块,同步代码块结束时,我习惯性的加入了notify语句,这样本身进入阻塞态的电梯就会被一直唤醒,wait阻塞又被唤醒以轮询;第二种情况可能为他们一前一后进入阻塞态时,在进入wait阻塞态之前有一个结束进程的判断,要调用PersonQuene中的isEnd方法,课程组在实验中给的代码中有notifyall,我不假思索默认是对的,不理解具体用法,导致两个线程不断唤醒对方均不能进入wait阻塞态。

  问题的根源在于我不理解sychnoized方法以及wait-notify原理,我原本认为当两个线程访问同步代码块或是同步方法时,拿到锁的线程进入,拿不到锁的默认执行wait方法,没有线程notify的话他就不会醒来,实际上这是错误的,只有执行了wait语句的才需要上述操作,没拿到锁暂时进入阻塞的线程无需notify方法就能继续执行。这也是导致如果不wait就会轮询的元凶。这个bug让我牵扯wa了一个点,起初还以为是评测有bug,怎么只会ctle一个点呢?以为再交一次就没事了,后来再提交时发现ctle了更多点,看来错一个反倒还是幸运的。

  问题的解决为删去不需要的notify即可,唤醒电梯线程的只应该为调度器加Person和最终结束唤醒,同楼座的一个电梯是绝对不应该唤醒另一个电梯的。

第三次作业

 

(FloorElevatorQuene和ElevatorQuene为废弃但未删除类,其实只相当于Arraylist)

 

架构模式:

  第三次作业参考第二次实验代码,所用的模型是调度器的单例模型,除了调度器大改以外,电梯类新增容量,速度等一系列属性,仿造实验新增Counter类检验任务完成,设立EndFlag以结束各线程。其余操作类似,仍是由inputhandler来承担解析输入,不过将实验中在main线程中检验任务完成调用acquire放在了inputhandler中,Inputhandler在接受完所有输入后开始检验各个任务,这么做的好处是无需将requestNum变量重新传回main线程,代码书写比较简单方便。另一个大改或者说巧妙之处在于对Person属性的改变,将原先的to,toBuilding抽象成目前的目的地,from,fromBuilding抽象为当前所在位置,这样在电梯类中就无需对策略操作进行改变,只需要在调度器对Person属性进行改变更新就行。

调度器设计

  第三次作业调度器参考实验,采用了单例模式,包含addPerson方法和与线程结束相关的属性和方法,核心还是addPerson及调度策略,Inputhandler和各电梯进程均会调用addPerson方法,调度器根据调度策略将该Person加入不同队列即可,同楼座电梯采用的仍是自由竞争这一策略,因为实现较为方便。由于调度器已不是线程,无需考虑调度器和电梯,输入控制器间共享对象的问题,但与之而来出现了一个新问题,不同电梯线程之间都会调用addPerson方法将要获得其它楼座电梯的锁,这会不会导致死锁呢?(具体分析将下方同步代码块和锁)

同步块和锁的设计:

  由于电梯类内并无大改,同步代码块也继承自上一次作业思路,在同步策略上应该不存在问题,唯一要解决的就是上文提到的将调度器改为单例模式时,不同楼座的电梯调用addPerson将人加入其他楼层队列时必然要获得该队列的锁,如果此时另一个电梯要获得这个电梯的锁,是否会发生死锁问题呢?好在我的架构上并不会出现这种情况,因为我的电梯在调用addPerson方法时,是从这个电梯的inPerson内部的人队列中拿出来的人Person,这个电梯内部的人并不是共享对象,所以只是一层锁。但如果调用addPerson时处在获得了整个楼座的waitPerson共享对象的锁的情况,就可能会发生死锁,如果楼座A,B(其实应该是一纵一横),同时调用addPerson方法,两者都先各自获得同楼座的waitQuene锁,接下来他们要将person加入对方楼座的waitQuene,A拿了A的锁想要B的锁,B拿了B的锁想要A的锁,二者都不撒手退让,进入死锁状态。

代码扩展能力:

  本次作业继承第二次作业,除了将调度器改单例模式以及增加调度策略外并无大改。这并无大改的前提是我巧妙利用了原有架构,将Person的to,from属性抽象为当前位置,当前目标等,再增加finalto,finaltoBuiding等属性描述最终目的地,这样可以使得电梯类无需大改,他的功能只需完成换乘中的一步即可,将调度过程交给新的单例模式调度器即可,可以说是非常方便。

性能优化:

  我认为本次作业在性能优化上必定天马行空,大显身手,但我的美好幻想并没有持续太久,我想出的很多优化思路并不好实现(怕出现更多线程安全问题),我对线程安全和算法的了解限制了我的思路。我最终采用的优化其实并不多,不过只是例如A-3到C-7先判断3层和7层能否横向到达,如不能在3-7层找一可以横向到达层,再不行就2,8,1,9,10这样逐步找。能否横向到达用一个三维Boolean数组来记录与判断。本来还想增加一个优化(后来发现是负优化或者不稳定优化?就放弃了),就是在执行换乘第一步任务时,增加该人的一个“虚拟人”,提前到达第一步预定位置,让下一步所在楼层的电梯来接他,但并不是实际“接上”,这么做能让下一步的电梯提前到达,如A-5-B-7在五楼换乘,B座电梯可以提前到达五楼,等人真正到了时接上即可,这在小数据量的情况下优化比较明显,但大数据量下经常是负优化。思考原因很久,应是这种优化几乎只能在B座电梯空闲时效果明显,如果B座电梯忙,这个请求很可能会在其他影响下被忽略,即使到了目标楼层,该电梯也有其他任务要执行很快离开,起不到加速效果,还有可能导致恶意竞争以降低速度,最终提交版本选择了放弃。

分析出现的Bug:

  本次bug测试是三次作业中最充分的,发现了程序中的很多bug,首先是默认from为低楼层,to为高楼层来写循环,全是i++形式,如果from高to低等于没进行调度分配,这个乘客就一直不会被电梯捎带。第二个错误也很典型,因为第一次作业是纵向调度,第二次作业是横向调度,第三次是换乘,我很自然的将乘客分为三类,想当然的认为只有第三类A-3-B-4这种需要让调度器进行换乘调度,殊不知本次作业中横向电梯开关门的楼座是可变的,有可能A-2-B-2不可通过横向电梯直达,如果将其种类分为第二类,默认继承上一次作业的方法,乘客就将一直得不到分配,解决的方法是将乘客分为两类,第一类就是同楼座,其他全分为第二类进行调度策略,不管是A-2-B-2还是A-2-B-3。其余的一些小错误就是横向电梯开关门上,需要加入能否停靠的特判,乘客上电梯也需要特判该电梯能否在这个乘客的目的站停靠,不能就不让其上车等。

  这次对自己程序进行的bug测试时间花费的是最长的,最终也起到了好效果,第一次强测互测都没有出现bug,强测得分也在98分,可以说努力总是有回报的。

强测和互测中均没有出现bug。

分析自己发现别人程序bug所采用的策略

  第一次作业时并没有参与互测hack,我还停留在第一单元,我是个垃圾的自我评价上,对同步方法和多线程理解也不深入,就没有参加,后来追悔莫及(线程输出不安全大部分人都没考虑)。看到同学朋友圈晒在同质bug修复前加了114分,我非常震惊,分外眼红,决心第二次要毅然决然加入。第二次互测hack数据16/48,采取的策略主要是对于线程不安全问题的hack,以及对一些激进的优化策略的hack,如边增加请求边增加电梯,对多部电梯多种方向在同楼层争抢多个人,对于线程不安全问题也是一边加人一边加电梯,保持每个楼层只有一部电梯运作,看是否会互相唤醒以及线程结束上判断等。本单元hack思路和上一个单元有很大不同,上一个单元主要在复杂性和边界条件上动手脚,这个单元最好的切入点是线程安全问题,复杂度和边界条件的构造其实效果并不好。

心得体会

  本单元的学习总算让我脱离了上个单元啥也不会,只会用预解析的阴霾,最大的不同还是对实验代码的理解和运用,上一单元完全单打独斗,对实验代码只有一点朦胧的印象,好似在哪看过这样的解决方法,思考如何解析表达式时完全没看实验代码,认为实验代码只是实验的一个小测试,和作业没什么大关联,现在想来真是非常生气,真的太蠢了,上一单元因为找不到参考而懊恼放弃,殊不知答案近在眼前。本单元则认真参考分析实验代码,往往在做实验的过程中就能理解其巧妙之处,下课后马上运用至自己代码中,甚至刻意等到礼拜四以后才正式开始做作业。

  除了对实验代码的理解运用外,本单元的学习中还通过网络和书籍学习多线程的有关知识,对于sychonized方法的理解和运用等,同时思考构思的时间也长了很多,经常能通过自己的认真思考不经意间就找出程序的bug,令我非常欣喜,这可能是平时对程序运行逻辑的思考增多带来的。

  本单元在测试得分上的表现也比较令我满意,对于面向对象课程的学习总算也是有了信心,不会像上个单元一样觉得自己啥也不会,只会用预解析,研讨课上同学的方法都好高级自己啥也不会,自怨自艾。本单元还在第二次作业的互测中大量参与了互测活动,效果也不错,总算增加了尝试的经验。同时,第二次互测的不错效果让我在第三次作业上也决心参与,提前一天就开始构造数据,对自己的程序进行测试,发现了自己程序的许多bug,非常不错。(但后来发现互测的限制条件改了,交上去数据很多不合法,星期天又有别的任务,第三次互测最后还是表现不佳)。

 

posted @ 2022-04-30 19:10  buaa_zzy  阅读(7)  评论(0编辑  收藏  举报