通过了三周多线程做业的的洗礼,平常在猝死边缘试探的我能有幸活到今天,先在此庆祝一下。虽然过程十分痛苦,但这三次多线程编程做业仍是让我受益良多。其中有一些矛盾给我留下了深入的印象,我把这些矛盾总结为如下3点:java
- 多线程的随机性与结果的准确性的矛盾
- 多线程的并发性与线程安全性的矛盾
- 线程的数量与程序性能的矛盾
这些矛盾的具体表现我将在下面的做业分析过程当中进行详细的描述。算法
第一次多线程做业正值清明假期,时间充足,我花了两天时间作了大量的前期准备工做,包括学习java多线程编程的有关知识以及做业的前期规划,我将前两次写的臃肿而丑陋的电梯进行了重构,保留原来的核心调度策略(双队列:请求队列,等待队列;主请求;按钮),几乎是重写了所有代码,虽然违背了做业要求继承上一次的意图。可是我以为此次重构是值得的,也是很有成效的。
此次做业使用了经典的生产者消费者模型,使用阻塞队列来保证共享数据的安全性,相通的部分我就再也不赘述,如下是我设计的调度器调度策略,电梯运行策略以及调度器和电梯之间的协同关系。
编程
程序主要由请求发生器,调度器,电梯三个类构成,三个类之间经过Request对象,阻塞队列和按钮开关来传递信息。
请求发生器负责根据控制台输入产生请求,判断请求格式的合法性,并将请求加入与调度器共享的阻塞队列。
调度器负责分配请求,其内有两个队列,一个是与请求发生器共享的请求队列,另外一个是等待队列。具体调度策略如前面的流程图所述。
电梯负责运行,根据当前的主指令和捎带队列中的指令定时调整自身的状态。
因为调度器须要获取电梯的状态进行判断,为了不出现线程安全问题,有的状态的get方法用synchronized作了同步,有的状态直接使用原子类和原子操做。
此次做业的结构上我还比较满意,因为把原来调度器的部分功能移到了电梯内部,电梯类显得有一些庞大,电梯类和调度器之间共享数据较多,使用了大量同步来避免线程安全问题。设计模式
此次做业因为准备极为充分,前期规划作的也很扎实,本身调试过程当中几乎没有遇到功能性bug,公测和互测也都没有出现bug。可是有一个矛盾十分明显,就是前面所述的第一条多线程的随机性与结果准确性的矛盾,因为请求的输入时间的不肯定性,形成电梯的运行实际上有至关程度的随机性,然而测试又须要准确的结果,这二者之间的矛盾几乎达到了不可调节的程度。主要冲突有两点:安全
因为程序运行自己是须要时间的,若是仅仅经过输出系统时间会致使程序运行时间一长就会产生累计偏差,因此不少同窗意识到这个问题以后都转向了假时间阵营(假时间就是在输出信息时使用的是一个经过起始时间通过一系列过程计算获得的一个绝对准确的时间),后来又衍生出了用理论须要睡眠的时间减去实际运行时间获得实际睡眠时间的“真假时间”阵营。
时间边界问题在前期是个人一个心头大患,举一个例子,若是有三台电梯理论上同时到达2层,但运动量各不相同,在到达以前有一条10层的楼层请求还未处理,按调度原则应该分配给运动量最小的电梯,但实际上这三台电梯并非真的同时到达,总会有几毫秒的偏差,这就会致使这条请求被分配给了第一个到达2层的电梯,而不是运动量最小的电梯。我在这个问题上的解决方案是:电梯到达某一楼层后向调度器发出分配请求信号,调度器收到信号先睡眠20ms再开始分配,这20ms能够确保三台电梯都已经到达了2层,从而解决了这个问题。固然这算是一个比较糟糕的解决方案,可是当时因为经验不足,即便是这样一个糟糕的解决方案也是与同窗们进行大量探讨后得出的少有的可行方案。在通过了出租车做业后我又有了一种新的解决方案——设置最小时间粒度,这个方法能够很好地解决时间边界的问题,这个方案将在出租车做业中进行详细介绍。
在互测阶段我拿到的那份代码,在运动量的处理上问题较为严重,公测就出现了问题,可是基本功能一切正常,只不过代码的结构与命名规范作的并很差,有UML协做图面条式代码的嫌疑,单调度器就有400行左右,可读性极差,我也就放弃了读代码的过程直接结合readme测试。通过一番狂轰滥炸,基本全部错误都与运动量相关,而这个点公测就已经挂了,因而只好上杀手锏,就是上一段所述的那个多台电梯都处于运行状态,多台电梯同时中止,中止时刻的请求分配问题。果不其然,分配给了第一台到达的电梯,出现了bug。
因为此次做业是第一次接触多线程,尚未讲线程安全的有关问题,也没有考虑如何测试线程安全相关问题,纯粹使用控制台本身把控时间进行输入,如今回想起来就以为当时真的太原始了,写一个测试类多简单。多线程
通过此次做业,我对多线程编程有了一个初步的认识,对生产者消费者模型也有了必定的了解。我我的认为此次做业的代码也从开学到当时写的最好的一次,不管是从每一个类的职责,各个类之间的协同关系,每一个方法的职责,以及总的代码量来看作的都比较出色。并发
此次做业是目前为止我认为最坑的一次做业,为何要用“坑”,主要缘由是若是论难度此次做业实际上与上一次做业没有明显的提高,可是此次的指导书简直就是天书,里面有太多的问题没有解释清楚。这直接致使我到周日的晚上尚未搞清楚此次做业到底要写个啥。无奈之下我只好结合指导书以及issue和客服群里各类五花八门甚至是自相矛盾的回答,运用“奥卡姆剃刀”(如无必要,勿增实体,即简单有效原则),作了一个总结,对各类触发器的触发条件以及对目录的监控以指导书为依托,把解释不通似是而非的部分所有“剃掉”,极大地简化了做业的难度,提出了对指导书的简化版理解。让我欣慰的是个人理解获得了助教的赞同,也得到了众多同窗的支持,以致于我发布的这个issue#32出如今了不少同窗的readme中,我在互测阶段拿到的这份做业就是这样。
此次做业开启了一个新的思路,就是状态快照。经过快照记录下某一瞬间的监控目录下全部文件的状态信息,按期拍摄快照并将两个相邻快照进行比对发现变化,因为快照中的文件信息的各个域都是在快照生成的瞬间就已经肯定下来不会改变的,因此在监视器访问快照的时候也就不存在线程安全问题。
因为此次做业实际上从周一开始才正式动工,时间很是紧迫,几乎没有前期规划,算法写的也不好,每一个路径上的一个触发器对应一个线程,每一个触发器的顶层目录拥有一份快照,全部快照经过一个HashTable进行管理,key值为目录文件自身或是普通文件的上层目录,减小了一部分重复的快照,可是仍是有至关多的冗余。全部触发器每隔1s唤醒一次,唤醒后保存以前快照而后刷新快照,将新的快照与以前保存的快照做对比,找不一样,看是否知足触发条件,若是知足执行对应任务,为了防止多个触发器同时执行任务致使一些莫明奇妙的事情发生 (recover任务最为明显),不得已对全部触发器线程作了互斥,另外一方面为了不文件类出现安全性问题对全部文件操做设置了静态锁,从而保证了同一时刻只能有一个线程在对文件进行操做,从而实现线程安全,这就扯出了前面说到的第二条矛盾:多线程的并发性与线程安全性的矛盾,个人这种处理方式明显是以牺牲并发性为代价的,因为将触发器类的run方法中除了sleep的部分所有都加了静态锁,实际上文件状态管理部分的全部触发器加起来就一个线程,再加上测试类一个线程,main一个线程,整个就三个线程,并无很好地利用多线程的并发性。函数
此次做业坑在指导书,程序自己难度不算太大,结构也不算太复杂。性能
此次做业的bug主观因素实在是太大,不少bug不是我本身程序写的有问题,而是改需求改出来的。
因为我上面所述的处理方式,以牺牲并发性为代价换取线程的安全性,基本上线程安全不会出现任何问题(至少通过我本身的大量测试没有问题),可是效率上明显不足,处理速度较慢,监控的文件一多就反应不过来,只能经过readme大法,强制要求两条测试之间sleep一段时间,这段时间根据触发器和监控文件的数目进行调整,至少sleep(1000),实际上我本身并无解决这个问题。还好给我测试的同窗遵照了这条规则,因此此次做业我也没有被抓到bug。
我测试的这份做业在公测阶段就出现了大量问题,主要表现是对文件的各类修改彻底没有反应。后来阅读代码发现这是因为同一个问题形成的,因为在全部触发器线程start以后没有作任何休息处理就启动了测试线程,触发器还没来得及创建初始快照,测试线程就已经对文件进行了修改,因此就捕捉不到对应的变化,算做是一个bug。另外一方面这份程序对指导书的理解在有些地方有较大出入,好比summary输出的是全部触发器触发的总次数,只能建立10个触发器而不是10个监控路径这两个问题。我拿到的这份做业要求两条请求之间sleep至少3s(比个人要求还过度...)这样实际上就很难再找到线程安全有关的问题了,不过看起来大部分同窗都是经过这个方法来避免线程安全问题的,最后的结果实际上和我牺牲并发换取线程安全异曲同工,甚至效率还没我高。(ps.经过写入超大数据延长文件操做时间引起线程安全问题,我以为不怎么道德,我也没用,不过经过文件操做加静态锁确实能够解决这个问题)学习
如何在保证线程安全性的同时维持并发是此次做业让我想的最多的一个问题。后来想的也许有点走火入魔,以为安全性优先,再加上公测和互测对结果的准确度要求极高容不得半点随机性致使的错误,并发性退居到很是靠后的位置,再加上某dalao的助攻,下次做业就走向了一条不归路...
我最初的想法是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条请求的一系列操做,性能上至关强劲,同时因为两个线程的互斥处理安全性也获得了保障。
程序有四个类主要的类:
出租车: 须要记录当前自身的位置, 状态, 是否有接单, 若是接单, 出发点和目的地在哪, 还要记录处于当前状态的时间。
请求: 也就是用户, 须要记录出发点, 目的地, 发出请求的时间, 请求是否已被分配,是否已经执行完毕, 记录请求窗口期中接单的全部出租车, 记录最终选定的出租车。
地图: 须要记录出租车在地图上的位置, 并记录全部出租车的状态。
调度器: 须要拥有地图和请求队列并记录时间。
除以上四个以外还有其余一些辅助的类, 好比 gui , 请求队列, 输入输出处理类。
对象之间的交互:
调度器:
a.按请求队列中的请求的出发点在地图上寻找范围内符合条件的出租车并分配给对应的请求。 200ms 一次。
b.实现将已经执行完的请求剔除。 200ms 检查全部请求并剔除其中执行完的请求。
c.更新整个系统的时间, 保证其余对象获取到的时间是正确的, 200ms 一次。
出租车:
a.调整本身的运行状态以及轨迹, 按照请求的要求进行移动。 200ms 一次
b.从地图获取最短路径矩阵, 从而实现按最短路径移动。 200ms 一次
c.从调度器得到当前时间, 200ms 一次
请求:
a.从调度器获取抢单的出租车, 200ms 刷新全部请求一次
b.从抢单的出租车中选择出最优的一个(即知足指导书要求的出租车)在到达窗口结束时间时
地图
a.为调度器查询出租车的位置提供数据和方法支撑, 200ms 可供全部请求每一个查询一 次
b.定时更新全部出租车的状态信息, 周期为 200ms
c.为出租车的运行提供地图邻接矩阵以及距离矩阵
因为该系统自己就十分庞大复杂,再加上提供的gui包结构混乱,致使整个度量结果看起来并不太好,类图也十分庞大(若是看不清楚右键点击查看图片,能够看清晰的图片)。
此次做业因为使用了上述的结构,避免了至关多的问题,不少其余同窗遇到的麻烦问题在我这都迎刃而解,好比使用这个结构,不会有时间上的累计偏差,性能强劲能够应对大规模的并发请求,全部出租车状态肯定不会出现莫名其妙的问题。从写完到最终提交实际上只是根据指导书的几个细节理解对输出格式以及部分状态的调整时间作了改动,并且修改过程都至关简洁,让我体会到了一个好的设计能够极大地简化后期维护的压力,并且在互测阶段也没有被发现bug。
互测时我拿到的这份代码,有几个很明显的问题,首先是命名规范,虽然大部分使用了英文命名,但其中有一些拼写错误,妨碍理解。另外一方面,在函数中声明的不少变量只是声明了但并无使用,在没有循环块的状况下常常出现没有任何做用的continue语句。读这份代码确实难度极大,但由于要自行编写测试代码,仍是耐着性子读下来了,读代码的过程当中就发现了一些问题,好比调度每分配一条请求后就会sleep(100),出租车每运动一步都sleep(200)。前者致使根本没法处理多条同时到达的请求,后者会产生积累偏差。这两个问题实际上均可以经过简单修改就能够避免。可是因为每100ms才会分配一条请求,极大地减小了系统的运行压力,掩盖了不少其余的问题,没有再发现其余bug。
此次做业,前期因为有dalao的帮助,在设计模式上下了很大功夫,作好前期设计后期正式开始写代码内心也就有了谱,相比IFTTT淡定了许多。我对课上所讲的设计模式也有了更深的体会。100辆出租车开一个线程也确实给我开启了一个新思路。
这三周能够说是我二十年来过得最痛苦的三周,高三都没有这么忙过,在OO和OS的双重压迫以及一堆琐事的干扰下,几乎天天晚上睡觉的时间都在凌晨一点半以后,周一周二基本上都是凌晨两点半以后,没有周末,甚至连清明假期都没过,就这样处于高度紧张状态持续了三周,中间几乎没有任何休息,常常在猝死的边缘试探,能够说也是对本身意志的一种磨练。我之因此可以度太重重难关,最主要的缘由其实是有众多dalao的帮助。从指导书理解,到bug调试,再到测试数据设计,在探讨的过程当中经过你们一块儿努力发现问题分析问题解决问题,各类新奇的想法互相权衡比对,优中选优。最后就不一一 @ 各位dalao了,谨在此致以衷心的感谢。