OO第二单元总结

OO第二单元总结

18373599 崔建彬

一、概述

  本单元作业主要是训练多线程编程能力。考察方案为调度一部或多部电梯完成任务。总体感受而言,比上次单元要相对容易。过度更为平整。

  我认为更为容易的原因有下:1.代码思维逻辑要求降低,更多的是训练多线程能力。2.一开始对架构思考较多,后续扩展性较好,不需要重构。3.边缘数据较少,出错更多的是超时或者线程安全问题,测试较为容易。

  我认为这个单元做的不好的地方有下:1.调度算法一般,只有第一次作业在强测中取得了良好的成绩。2.架构仍然不够好,编程过程中,代码仍有 “这个类管了其他类应该管的事” 这种现象发生。

二、设计策略分析

1.第一次作业为单电梯运送。

  采用课程组推荐的生产者-消费者模式。生产者为Input类,单独开一个线程,向安全队列中写入需求。传送带为一个安全队列,相应需要访问的函数均加上锁。消费者为单电梯。采用的调度方式为基于sstf改进的贪心算法。

  

注:在这次作业中,基于sstf改进的贪心算法可以取得较好的分数。

 

2.第二次作业 新增多部电梯要求及新增电梯需求。

  我采取的策略为:在原有基础上新增调度器,动态的分配需求给多部电梯。分配策略为:电梯距离当前要乘坐的人最近的,获取该需求。

  注意:本次作业并未取得非常好的性能分。

 

3.第三次作业为:新增限制楼层,新增楼层,新增添加电梯需求。

  我在第二次作业基础上并没有做大的改进。采用静态的方法分配楼层.

  即,能不换乘就不换乘。需要换乘的时候,将目标拆分为两个队列,分别加入两个电梯。新建Person类。用来记录个人需求的同时,记录该人是否需要换乘,是否被拆分,拆分的第二个请求是什么。如果需要被拆分,当第一个请求完成时,将其第二个请求加入请求队列。(注:本次作业所有需求都可以拆分成两个来完成。)

  结束电梯方法:每新增一个请求,就在HashMap中记录,每一个人从电梯中出来,将HashMap这个人的id对应的状态设置为完成。当所有人都从电梯中出来时候,电梯结束。

  这样做的缺点就是:性能分依然平庸。但是方便修改,(就是写完的快)。

 

三、架构设计的扩拓展性

  总体设计原则依照:消费者-生产者模式。生产者为Input类,即读取需求。消费者为电梯。

  第二三次作业设计原则也有根据:工人-监工模式。即调度器为监工。电梯为工人。电梯向调度器反馈信息。同时调度器向电梯分配任务。电梯只需要根据sstf算法,运送电梯等待队列中所有人即可。调度器只需要根据每个电梯完成任务情况,分配新的任务即可。

  整体有较好的拓展性。表现为:每次不需要修改原有类,只需新增函数即可满足需求。如第一次到第二次作业,新增调度器,稍微修改电梯楼层限制。第三次作业,新增调度器算法,修改电梯楼层限制,即可满足要求。每次完成新增需求时间较短。且正确性得以保证。

根据Solid罗列问题:

  违反了SOLID之SRP: 即每个类或方法都应只有一个明确的职责。

  1.我的电梯类,不仅有运输人的功能,还有自带的SSTF算法,算是抢占了调度器的一部分职责。

  2.电梯获取最近FromFLoor时候,是使用安全队列中的函数。即安全队列负责搜寻一部分主请求,这很明显抢占了电梯的职责。

 

四、基于度量分析自己的程序结构

第一次作业:

  从这两个数据来看,可以看出第一次作业整体难度不高。主要是电梯承担了调度器的调度任务,复杂度过高,这也是本单元作业的不足之处之一。

  同时,从UML图中可以看出。第五次作业只有两个线程。一个安全队列。线程Inputhandle向安全队列中输入请求。Elevator线程从安全队列中处理请求。

第二次作业

 

 

 

 

  第二次作业总体来说较第一次作业增加较少。无论是代码长度还是逻辑上。但是新增的类:调度器,也显得较为臃肿。主要新增功能为,增加调度器来使用多电梯。

注: 因为第二次作业和第三次作业整体相似度较高,所以这里只摆放第三次作业的相关分析。

第三次作业

 

 

 

  第七次作业代码量较前几次还是有提升。甚至比第一单元第三次作业量还大(然鹅我本人觉得第一单元第三次作业量最大,可能是因为重构的原因。这也体现出了认真架构有多么重要,代码量直接小几倍)。

  从复杂度中可以看出,调度器和电梯都有点复杂度超标了。他们管理了一些不是他们该管的事情。本来我的思路是主调度器+各级调度器。可以明显看出,电梯和主调度器分别占了各级调度器的一些功能。所以较为冗长。而这么做的主要原因是,不想重构。(从这也可以看出,我要是第一次就做好一个调度器管电梯是多么重要。)

  第三次作业的线程合作是:输入函数是一个线程,向一级对列输入请求。调度器是个线程(其实这里可以不用线程)。负责将一级对列中的数据输入各电梯的二级对列。各个电梯也是线程,负责处理请求。他们之间的安全队列均为多次实例化RequestQueue这一手动构造的安全队列。

五、分析自己程序的BUG

  1. 在三次作业的强测中,均为出现bug。

  2. 在第三次作业的互测中,电梯调度算法有不足,因为程序运行时间过长被HACK了。还是没有死锁的那种,单纯没跑完。这点很受打击。一方面是自己在HACK别人时,没有想到这个。另一方面是自己的性能竟然拉胯到如此程度。

  3. 不过言归正传,我认为这个单元作业出现上错楼/电梯调用错等错误是几乎不可能的。我自己本人所面临的最多的bug都是和线程相关的。同时线程相关的问题一般比较难以处理,因为很难在本机上复现。IDEA在运行程序时候,会穿插很多其他线程,导致你的bug复现不出来是非常正常的。最常见的可能就是死锁和电梯无法终止。

    1. 死锁:死锁其实发生我认为还是较为困难的。因为我是采用,一部分一部分测试。每完成一部分函数/新增功能,都会测试。如果死锁,较为容易看出来。但是仍然在互测中,发现有些同学的部分程序会在特定输入时死锁。

    2. 电梯无法终止:这个问题,反而是第二单元最困扰我的问题。因为在第三次作业中,需要拆分请求。导致我判断电梯停下来的方式是轮询。即一遍一遍让调度器跑去问电梯:你停了没有?结局也很简单,CTLE了。后来,我记录所有电梯进入队列的人和出电梯的人。在电梯没有需求的时候就wait,当所有进来的人都出去以后,就关闭所有线程。

    找到并修正BUG:在本单元的测试中。出现bug不一定能本地复现,本地复现提交测评不一定能HACK。这其中有一系列玄学因素在其中。但这并不是说我们无法解决被HACK的线程问题。毕竟,如果你的线程如果一点也没错,我相信在强测和互测中,你都不会出问题。

    所以,我才用检查线程的方法也很简单:1.现在本地测试,如果是直接复现出来的,那当然最好。2.将数据拷贝几百次,放到评测机里跑。3.如果还是无法复现,那就分析数据可能测到的bug是哪里,然后开始脑测:肉眼扫代码, 看看是不是每个wait的线程,都可以被唤醒。每个没加锁的共享数据,是否会有危险。或者更干脆的方法:在写代码前想清楚这些问题,并做相应的记录,这样后续会省很大的力气。

 

六、分析自己发现别人程序BUG所采取的策略。

1、评测机狂跑:

  这是我最常用的一种策略。这里提名感谢wpb同学,让我既可以多线程又可以多进程跑评测机。大量数据覆盖上去,如果跑个一整天都没问题,几千个数据都通过了,我觉得通过强测数据还是没问题的。无论是自己的程序还是别人的程序,在自己忙的时候都可以用评测机来测试。

2、边缘数据:

  任何单元测试,我都是自己想边缘数据+评测机的策略来测试的。毕竟评测机生成情况有限,总有无法生成的数据。这种数据一方面自己测试时候要保留下来,方便以后HACK。另一方面,可以多与同学们交流互换,毕竟自己脑想数据量总是有限的。

  我个人记录了一些边缘数据。比如两条指令相隔很久,来试试是否有人clte。只有增加电梯一个指令,看看程序是否能够停下来。同时输入大量同楼层数据,看看该同学调度算法是否会过慢。(我就是被这么HACK了)

  但是最后还是翻车了:在大部分时候,我是hack不到别人的bug。我觉得主要原因是1.找到的bug多为线程时序bug。在评测机可能无法触发。2.较少的人出现WRONG ANSWER。这种最容易hack到的bug出现较少。

  最后:建议同学们(来年学弟)可以多试试hack性能分,(对就算210s,也可以hack)。大概率可以有收获。(比如33s疯狂进一大堆人只坐A电梯)。

 

七、心得体会:

  其实该内容和第一部分概述有点相似。这个单元整体给我的感觉是,难度较上一单元低。迭代所需要的时间更少(毕竟不用重写)。整体测试也相对容易(因为边缘数据相对较少且相对容易想到和构造。)所以这个单元最难的地方应该是入门多线程和对多线程常见问题的理解。

  对我个人来说,最难的地方应该是对锁的理解。在大量阅读讨论区配合作业和试验以后,我确实对锁有了进一步的认识,同时还见识到了Lock(),unlock()这种更加清晰的方式。在作业调试中,我也比较幸运,几乎没遇到无法复现的问题。有的话,也是通过肉眼扫代码逻辑,较快地查处了问题所在。同时,也进一步加深了对锁的理解。

  总体来说,我从一个学生角度,觉得这个祖传的电梯单元设计的还是很成功的。没有难度的陡增,也在一开始授课时,告诉了我们架构的思路,迭代也更为平滑,不需要重构,把更多的时间用在了研究多线程上。所以说我觉得还是受益匪浅的一个单元。

 

原文地址:https://www.cnblogs.com/cjbbb/p/12719462.html