OO第二次博客作业

OO第二次博客作业

写在前面

  经过了三周多线程作业的的洗礼,日常在猝死边缘试探的我能有幸活到今天,先在此庆祝一下。虽然过程十分痛苦,但这三次多线程编程作业还是让我受益良多。其中有一些矛盾给我留下了深刻的印象,我把这些矛盾总结为以下3点:

  1. 多线程的随机性与结果的准确性的矛盾
  2. 多线程的并发性与线程安全性的矛盾
  3. 线程的数量与程序性能的矛盾

这些矛盾的具体表现我将在下面的作业分析过程中进行详细的描述。

多线程电梯

1.设计策略

  第一次多线程作业正值清明假期,时间充足,我花了两天时间做了大量的前期准备工作,包括学习java多线程编程的有关知识以及作业的前期规划,我将前两次写的臃肿而丑陋的电梯进行了重构,保留原来的核心调度策略(双队列:请求队列,等待队列;主请求;按钮),几乎是重写了全部代码,虽然违背了作业要求继承上一次的意图。但是我觉得这次重构是值得的,也是颇有成效的。
  这次作业使用了经典的生产者消费者模型,使用阻塞队列来保证共享数据的安全性,相通的部分我就不再赘述,以下是我设计的调度器调度策略,电梯运行策略以及调度器和电梯之间的协同关系。

  程序主要由请求发生器,调度器,电梯三个类构成,三个类之间通过Request对象,阻塞队列和按钮开关来传递信息。
  请求发生器负责根据控制台输入产生请求,判断请求格式的合法性,并将请求加入与调度器共享的阻塞队列。
  调度器负责分配请求,其内有两个队列,一个是与请求发生器共享的请求队列,另一个是等待队列。具体调度策略如前面的流程图所述。
  电梯负责运行,根据当前的主指令和捎带队列中的指令定时调整自身的状态。
  由于调度器需要获取电梯的状态进行判断,为了避免出现线程安全问题,有的状态的get方法用synchronized做了同步,有的状态直接使用原子类和原子操作。

2.程序结构分析




这次作业的结构上我还比较满意,由于把原来调度器的部分功能移到了电梯内部,电梯类显得有一些庞大,电梯类和调度器之间共享数据较多,使用了大量同步来避免线程安全问题。

3.bug分析

  这次作业由于准备极为充分,前期规划做的也很扎实,自己调试过程中几乎没有遇到功能性bug,公测和互测也都没有出现bug。但是有一个矛盾十分明显,就是前面所述的第一条多线程的随机性与结果准确性的矛盾,由于请求的输入时间的不确定性,造成电梯的运行实际上有相当程度的随机性,然而测试又需要准确的结果,这两者之间的矛盾几乎达到了不可调节的程度。主要冲突有两点:

  1. 使用假时间还是系统时间
  2. 时间边界的处理

  由于程序运行本身是需要时间的,如果仅仅通过输出系统时间会导致程序运行时间一长就会产生累计误差,所以很多同学意识到这个问题之后都转向了假时间阵营(假时间就是在输出信息时使用的是一个通过起始时间经过一系列过程计算得到的一个绝对准确的时间),后来又衍生出了用理论需要睡眠的时间减去实际运行时间得到实际睡眠时间的“真假时间”阵营。
  时间边界问题在前期是我的一个心头大患,举一个例子,如果有三台电梯理论上同时到达2层,但运动量各不相同,在到达之前有一条10层的楼层请求还未处理,按调度原则应该分配给运动量最小的电梯,但实际上这三台电梯并不是真的同时到达,总会有几毫秒的误差,这就会导致这条请求被分配给了第一个到达2层的电梯,而不是运动量最小的电梯。我在这个问题上的解决方案是:电梯到达某一楼层后向调度器发出分配请求信号,调度器收到信号先睡眠20ms再开始分配,这20ms可以确保三台电梯都已经到达了2层,从而解决了这个问题。当然这算是一个比较糟糕的解决方案,但是当时由于经验不足,即使是这样一个糟糕的解决方案也是与同学们进行大量探讨后得出的少有的可行方案。在经过了出租车作业后我又有了一种新的解决方案——设置最小时间粒度,这个方法可以很好地解决时间边界的问题,这个方案将在出租车作业中进行详细介绍。
  在互测阶段我拿到的那份代码,在运动量的处理上问题较为严重,公测就出现了问题,但是基本功能一切正常,只不过代码的结构与命名规范做的并不好,有UML协作图面条式代码的嫌疑,单调度器就有400行左右,可读性极差,我也就放弃了读代码的过程直接结合readme测试。经过一番狂轰滥炸,基本所有错误都与运动量相关,而这个点公测就已经挂了,于是只好上杀手锏,就是上一段所述的那个多台电梯都处于运行状态,多台电梯同时停止,停止时刻的请求分配问题。果不其然,分配给了第一台到达的电梯,出现了bug。
  由于这次作业是第一次接触多线程,还没有讲线程安全的有关问题,也没有考虑如何测试线程安全相关问题,纯粹使用控制台自己把控时间进行输入,现在回想起来就觉得当时真的太原始了,写一个测试类多简单。

4.心得体会

  经过这次作业,我对多线程编程有了一个初步的认识,对生产者消费者模型也有了一定的了解。我个人认为这次作业的代码也从开学到当时写的最好的一次,不论是从每个类的职责,各个类之间的协同关系,每个方法的职责,以及总的代码量来看做的都比较出色。

IFTTT

1.设计策略

  这次作业是目前为止我认为最坑的一次作业,为什么要用“坑”,主要原因是如果论难度这次作业实际上与上一次作业没有明显的提升,但是这次的指导书简直就是天书,里面有太多的问题没有解释清楚。这直接导致我到周日的晚上还没有搞清楚这次作业到底要写个啥。无奈之下我只好结合指导书以及issue和客服群里各种五花八门甚至是自相矛盾的回答,运用“奥卡姆剃刀”(如无必要,勿增实体,即简单有效原则),做了一个总结,对各种触发器的触发条件以及对目录的监控以指导书为依托,把解释不通似是而非的部分全部“剃掉”,极大地简化了作业的难度,提出了对指导书的简化版理解。让我欣慰的是我的理解得到了助教的赞同,也获得了众多同学的支持,以至于我发布的这个issue#32出现在了很多同学的readme中,我在互测阶段拿到的这份作业就是这样。
  这次作业开启了一个新的思路,就是状态快照。通过快照记录下某一瞬间的监控目录下所有文件的状态信息,定期拍摄快照并将两个相邻快照进行比对发现变化,由于快照中的文件信息的各个域都是在快照生成的瞬间就已经确定下来不会改变的,所以在监视器访问快照的时候也就不存在线程安全问题。
  由于这次作业实际上从周一开始才正式动工,时间非常紧迫,几乎没有前期规划,算法写的也很差,每个路径上的一个触发器对应一个线程,每个触发器的顶层目录拥有一份快照,所有快照通过一个HashTable进行管理,key值为目录文件自身或是普通文件的上层目录,减少了一部分重复的快照,但是还是有相当多的冗余。所有触发器每隔1s唤醒一次,唤醒后保存之前快照然后刷新快照,将新的快照与之前保存的快照作对比,找不同,看是否满足触发条件,如果满足执行对应任务,为了防止多个触发器同时执行任务导致一些莫明奇妙的事情发生 (recover任务最为明显),不得已对所有触发器线程做了互斥,另一方面为了避免文件类出现安全性问题对所有文件操作设置了静态锁,从而保证了同一时刻只能有一个线程在对文件进行操作,从而实现线程安全,这就扯出了前面说到的第二条矛盾:多线程的并发性与线程安全性的矛盾,我的这种处理方式明显是以牺牲并发性为代价的,由于将触发器类的run方法中除了sleep的部分全部都加了静态锁,实际上文件状态管理部分的所有触发器加起来就一个线程,再加上测试类一个线程,main一个线程,整个就三个线程,并没有很好地利用多线程的并发性。

2.程序结构




这次作业坑在指导书,程序本身难度不算太大,结构也不算太复杂。

3.bug分析

  这次作业的bug主观因素实在是太大,很多bug不是我自己程序写的有问题,而是改需求改出来的。
  由于我上面所述的处理方式,以牺牲并发性为代价换取线程的安全性,基本上线程安全不会出现任何问题(至少经过我自己的大量测试没有问题),但是效率上明显不足,处理速度较慢,监控的文件一多就反应不过来,只能通过readme大法,强制要求两条测试之间sleep一段时间,这段时间根据触发器和监控文件的数目进行调整,至少sleep(1000),实际上我自己并没有解决这个问题。还好给我测试的同学遵守了这条规则,所以这次作业我也没有被抓到bug。
  我测试的这份作业在公测阶段就出现了大量问题,主要表现是对文件的各种修改完全没有反应。后来阅读代码发现这是由于同一个问题造成的,由于在所有触发器线程start之后没有做任何休息处理就启动了测试线程,触发器还没来得及建立初始快照,测试线程就已经对文件进行了修改,所以就捕捉不到对应的变化,算作是一个bug。另一方面这份程序对指导书的理解在有些地方有较大出入,比如summary输出的是所有触发器触发的总次数,只能创建10个触发器而不是10个监控路径这两个问题。我拿到的这份作业要求两条请求之间sleep至少3s(比我的要求还过分...)这样实际上就很难再找到线程安全有关的问题了,不过看起来大部分同学都是通过这个方法来避免线程安全问题的,最后的结果实际上和我牺牲并发换取线程安全殊途同归,甚至效率还没我高。(ps.通过写入超大数据延长文件操作时间引发线程安全问题,我觉得不怎么道德,我也没用,不过通过文件操作加静态锁确实可以解决这个问题)

4.心得体会

  如何在保证线程安全性的同时维持并发是这次作业让我想的最多的一个问题。后来想的也许有点走火入魔,觉得安全性优先,再加上公测和互测对结果的准确度要求极高容不得半点随机性导致的错误,并发性退居到非常靠后的位置,再加上某dalao的助攻,下次作业就走向了一条不归路...

扫描二维码关注公众号,回复: 90998 查看本文章

出租车

1.设计策略

  我最初的想法是100辆出租车开100个线程,调度器开一个线程,输入开一个线程,基本模式与多线程电梯相仿,只不过把一维运动改为二维运动,但是仔细一想就觉得有问题,之前电梯开3个线程运行时间一长就有积累误差,这次100辆出租车,误差就更难以避免了。这就引出了上面提到的第三个矛盾线程的数量与程序性能的矛盾。我在第三次OO上机实验时尝试过开100个线程的效果,大部分的时间都用在了上下文切换上,运行速度不比开5个线程快,而且这次作业出租车运行一格的时间仅有200ms,对性能的要求极高,不允许有卡顿现象的发生,200ms内必须要调整100辆出租车的状态,并且还要进行相应请求的分配,开100个出租车线程显得太多了,也有些不切实际。
于是就有走上了一条不归路的说法。说是不归路,但我觉得走好了也可能是一条通天大道。这个方案来自某dalao。再回过头分析一下之前的多线程电梯作业中的两个问题,一个是假时间,一个是时间边界,这两个问题仅依靠多线程来处理是几乎无解的,难免会出现误差,这是多线程自身的随机性所导致的。而测试不允许这种随机性误差的发生(这也算是课程要求的一个缺陷吧,希望以后在公测和互测阶段能够有所改进),为了确保万无一失,需要一个所有状态都是确定的多线程。
  分析这次的指导书,最小时间单位为100ms,出租车运行一格需要200ms。100ms主要是为了区分请求,最关键的实际上在这个200ms上,出租车一开始从0ms时刻开始运行,每次所有出租车处于地图上某个确定的点上的时刻必定是200ms的整数倍,由于每条请求实际上最终都要落实到出租车去执行,所以实际上请求开始执行和结束的时间也必定是200ms的整数倍。所以只需要每200ms扫描一遍出租车列表,扫描一遍请求队列就可以了,而且要确保扫描请求队列的时候所有出租车的状态都已经确定了,于是就有了下面的方法。
  100辆出租车整体作为一个线程,调度器作为一个线程,每200ms扫描这100辆出租车,并调用每辆出租车改变自身状态和位置的函数,在所有出租车的状态都改变之后唤醒调度器,进行请求的分配,此时所有出租车的状态都已经确定了,绝对不会出现开一百个线程可能会出现的在分配请求时部分出租车已经完成了移动,另一部分出租车还没完成移动的情况发生。调度器与出租车列表两个线程进行了互斥处理,确保两个线程交替进行,睡眠时间调整为200ms减去其真实运行的时间,从而保证不会出现累计误差。
  为了进一步缩短运行时间,我将所有出租车挂在其位置对应的地图点上,请求到来时,调度器只需要搜索请求出发点为中心的25个点即可,免去了遍历100辆出租车并进行比较的麻烦。另一方面,我将地图上任意两点间的最短距离在初始化时提前算好,之后直接拿出来用。经过这一系列的处理,即使是同时有300条请求到达,在我这个低压双核i7上也可以在50ms内完成遍历出租车以及分配这300条请求的一系列操作,性能上相当强劲,同时由于两个线程的互斥处理安全性也得到了保障。

2.程序结构




由于该系统本身就十分庞大复杂,再加上提供的gui包结构混乱,导致整个度量结果看起来并不太好,类图也十分庞大。

3.bug分析

  这次作业由于使用了上述的结构,避免了相当多的问题,很多其他同学遇到的麻烦问题在我这都迎刃而解,比如使用这个结构,不会有时间上的累计误差,性能强劲可以应对大规模的并发请求,所有出租车状态确定不会出现莫名其妙的问题。从写完到最终提交实际上只是根据指导书的几个细节理解对输出格式以及部分状态的调整时间做了改动,而且修改过程都相当简洁,让我体会到了一个好的设计可以极大地简化后期维护的压力,而且在互测阶段也没有被发现bug。
  互测时我拿到的这份代码,有几个很明显的问题,首先是命名规范,虽然大部分使用了英文命名,但其中有一些拼写错误,妨碍理解。另一方面,在函数中声明的很多变量只是声明了但并没有使用,在没有循环块的情况下经常出现没有任何作用的continue语句。读这份代码确实难度极大,但因为要自行编写测试代码,还是耐着性子读下来了,读代码的过程中就发现了一些问题,比如调度每分配一条请求后就会sleep(100),出租车每运动一步都sleep(200)。前者导致根本无法处理多条同时到达的请求,后者会产生积累误差。这两个问题实际上都可以通过简单修改就可以避免。但是由于每100ms才会分配一条请求,极大地减少了系统的运行压力,掩盖了很多其他的问题,没有再发现其他bug。

4.心得体会

  这次作业,前期由于有dalao的帮助,在设计模式上下了很大功夫,做好前期设计后期正式开始写代码心里也就有了谱,相比IFTTT淡定了许多。我对课上所讲的设计模式也有了更深的体会。100辆出租车开一个线程也确实给我开启了一个新思路。

总结

  这三周可以说是我二十年来过得最痛苦的三周,高三都没有这么忙过,在OO和OS的双重压迫以及一堆杂事的干扰下,几乎每天晚上睡觉的时间都在凌晨一点半之后,周一周二基本上都是凌晨两点半之后,没有周末,甚至连清明假期都没过,就这样处于高度紧张状态持续了三周,中间几乎没有任何休息,经常在猝死的边缘试探,可以说也是对自己意志的一种磨练。我之所以能够度过重重难关,最主要的原因实际上是有众多dalao的帮助。从指导书理解,到bug调试,再到测试数据设计,在探讨的过程中通过大家一起努力发现问题分析问题解决问题,各种新奇的想法互相权衡比对,优中选优。最后就不一一 @ 各位dalao了,谨在此致以衷心的感谢。

猜你喜欢

转载自www.cnblogs.com/panxuchen/p/8971514.html