oo5_7html
多线程电梯时,我还执着于时间的精准性,也就是上下楼必定要多少多少秒,因此采起的是假时间策略。java
为了实现假时间策略,我将三部电梯的运行封闭到了一个线程当中,单独一个线程内部的执行是不会受到线程调度产生的偏差的影响的。git
在这基础上,考虑到输入IO会有阻塞,安排了一个输入线程。至于调度器线程,对于使用假时间策略的个人设计而言,它是无关紧要的,毕竟实际的调度都是在电梯线程内部进行的,若是不在电梯线程内部进行,那么调度与执行之间就会产生线程调度致使的时间偏差,会致使正确性问题。故而个人调度器线程虽然按照指导书要求而加上了,但它仅进行部分不会产生时间偏差的调度功能。github
在输入线程与调度器线程之间仅有指令须要传递,我才用了一个阻塞队列来实现指令队列。利用java库中自带的同步容器类保证线程安全。web
在调度器线程与电梯线程之间,存在两类同步问题:canvas
第一类同步问题,由于我采用的是电梯线程内部假时间的策略,因此很容易解决,只须要利用一把调度器线程与电梯线程共享的“运行锁”便可。电梯线程每次主循环开头获取锁,主循环末尾释放锁。当调度器线程但愿中止电梯线程时,只需获取该锁便可。同时为了不饥饿问题,这里我采用了公平锁,在只有两个线程争夺该锁的状况下,性能损失仍是能够接受的。缓存
第二类同步问题,由于当调度器线程访问电梯状态时,电梯线程一定中止,不可能更新自身状态,故而仅需确保电梯状态都能反映到内存中,而不是被缓存。故而我大量使用了volatile变量、原子变量实现轻量级的同步。安全
线程划分很是明细、简单:ruby
首先考虑触发器线程组,它们之间共享的是被监控的文件,而这份线程安全性被委托给了FileCenter这一线程安全的File类的封装类。服务器
再考虑summary线程、record线程,它们之间没有共享,但各自都和许多触发器线程共享了它们记录的信息。这是典型的“读者-写者”的状况。写者是一堆触发器线程,读者是须要将记录的信息写进文件的summary、record线程。我采用了消息队列的方法保证了写者与读者的同步,将线程安全性委托给了java的同步容器类。
这一次我抛弃了多线程电梯时注重正确性的策略,没有采用假时间策略。故而这里100个出租车再也不只有一个线程,而是真正的100个线程。
同时,我注意到对乘车请求的响应、“抢单窗口”的设计很是适合使用服务器模型进行实现。故而我安排了一个线程池,这个线程池中一个线程对应于正在处理的一个乘车请求,称该线程为调度单元线程。
除此以外,就还有一个标配的输入线程。
首先,出租车之间共享地图,以及调度单元。
其次须要保证出租车的状态对其余线程均可见,这一点经过简单的内部锁便可实现。
最后由于须要经过位置、状态来访问出租车,故而我安排了一个缓冲用的TaxisMonitor,出租车监控类来存储缓存信息。由于该缓冲对象会被全部出租车访问来更新缓存,故而须要进行同步。这里我采起了细粒度加锁策略,毕竟自己就是为了性能而作的缓冲,不能由于加锁反而损失性能。对于每个位置上一个锁,每一种状态上一把锁。
不过由于每一次出租车状态更新会须要访问先后两个状态,若是同时获取两个状态的锁,会致使死锁问题。个人解决方法是让程序同一时间要么只获取前一状态的锁,要么只获取后一状态的锁,虽然会致使出租车在一段时间内在缓冲区中不可见,但能够简单解决了死锁问题。而不可见致使的正确性损失,在此次做业中并没有伤大雅。若是真的会由于这点时间的不可见而产生正确性错误,那出租车线程自己运行的时候就会由于过卡而致使走一条边超过200s了。
此次电梯做业光看度量的面板数值还能够,也就输入处理那里我图省事嵌套多了点。可是实际上由于是屡次更迭的项目,其中有众多冗余的代码。这点从55个类、2898行代码中就能够看出。
从面板数值上能够看出,此次IFTTT做业最大的问题就是,它的分支判断至关地庞大。这一点我实在想不出怎么避免,我已经将分支判断尽量封装在一个方法中,而且保证该方法的接口统一性。或许能够采用将分支判断数据化,而后编写自动进行分支判断的代码来解决。
状态变化——这是此次出租车的红点。我在思考可否经过将状态自己也给抽象出来,做为一个类对待?而后状态变化的逻辑交由状态自己来处理?或许这样就能将复杂的圈逻辑降维,分散到各个状态类中去。
从LiftsThread以右下的那一部分代码所有都是和单线程时一致的,也就是电梯内部依然是按照单线程时的运做模式进行运做,再也不赘述其内部实现。
为了可以复用单线程时的代码,我在LiftsThread内部,将系统时间的流逝转化成了对Lift响应模拟时间变化的调用次数。也就是每过几几秒就调用一次Lift一个时间粒度的变化函数。
因此实际上,LiftsThread仅仅只是一个用来封装模拟时间的线程,其内部不包含任何调度逻辑。
实际的调度逻辑所有被包含在Schedular及SubSchedular中。其中SubSchedualr为单部电梯时的调用逻辑。Schedular为协调三部电梯的运动量均衡策略逻辑。
SchedularThread仅仅是为了迎合指导书要求而赘写的中介线程。
InputThread负责读入指令,并将其放入CommandTray中。以后SchedularThread从CommandTray中取出指令,再暂停LiftsThread,转交给它指令。
World负责系统内时间的管理。
ifttt中我大量使用了继承,主要缘由是指导书的不明确以及来自助教的需求的频繁变动,致使了代码须要不断维护。为了减小代码维护时的工做量,我尽量地复用代码,减小同质代码的出现。
继承树一共有四支。
FileCenter即为一个线程安全的File类的封装类,负责文件读写的底层封装。
出租车的类设计主要分为了三个族:
Taxi族。这部分包含了Taxi, Driver, TaxisMonitor。这一族内部高度耦合,三位一体地实现了出租车。
Timepasser为Taxi的基类,封装了Taxi的线程逻辑。负责将系统时间的流逝转化成Taxi内部时间的每时间粒度的流逝。
Schedule族。这部分包含了ScheduleServer, ScheduleUnit。ScheduleServer维护了一个线程池,该线程池中每个线程都为ScheduleUnit的实例化。这一族负责响应乘车请求,并对每一个乘车请求分配一个线程进行抢单、分单等逻辑。
除了这三族之外,还有几个类是为了以后的扩展而额外实现的:
关于这个,我难以进行分析……由于我不知道我有没有作到。究竟作到什么程度才能算是作到了某一项原则?我没有足够的经验去回答这个问题……我只能说我尽可能去作了。
主要的bug来自于对指导书理解有误以及未及时看issue和微信群。其次的bug大可能是由于我没写正则去检查输入是否合法,只要输入错了,那我就挑正确的而后继续跑,跑不下去再跑错,可是公测要求无论能不能继续跑都要报错。
我没怎么查别人的bug,没那个时间,仅仅只是按照公测要求完成了课程安排的工做。
其实多线程的同步控制并不困难,与电梯复杂的调度逻辑相比,那是很简单的东西。代码编写过程当中最大的难点实际上是两点:
我很想说我知道这个问题的答案,但我作不到。仅仅只是阅读指导书,进行需求分析,并不能真正导向“正确的代码”,而只是能接近。在知道这件事情之后,我所能作的或许仅仅只是留出“将代码改正确的余地”。
在程序写完前我无从得知程序的性能如何——这很显然,可是我却必需要写完它。若是我在编写过程当中就考虑性能问题,那颇有可能就会致使我代码根本写不完,或者越写越错。由于优化性能的代码逻辑每每是复杂的。在第一次编写的过程当中我每每会采起最简单的那种方法,而不是最快的方法。我所能作的或许仅仅只是留出“将代码变快的余地”。
上述两个问题最终都导向一点:余地。也就是代码的可修改空间、可拓展空间。当我编写的程序不仅是写完就行,过了OJ就行的时候,“余地”成了我编写代码时须要重点考虑的因素。
在电梯的时候,我十分固执于程序的正确性,经过不断地发issue明确指导书的意思,我虽然不能彻底作到,可是却能向“正确的程序”接近。
不过在ifttt时,需求频繁的变动、指导书的不明确让我放弃了对正确性的固执——那是一件作不到的事情了,一样一份代码,两我的看,一我的以为对,另外一个以为错,再也不有统一规定。
公测逐渐转为互测,互测时bug树能够用一个bug挂满,许多人的公测由于对方没测而满分,吐槽版各类认领代码,等等现象,让我明白了一件事情:这课的成绩没有很大的意义,就和个人博雅课程的成绩的意义差很少。虽然这课的成绩会算进GPA,可是这课的设计就不是以给学生成绩为核心的。
言尽于此,并非抱怨,这就是我所见到的,我所想的。