标签:功能 processor 过程 答案 编写 消息 程序 微信群 运行
oo5_7
多线程电梯时,我还执着于时间的精准性,也就是上下楼一定要多少多少秒,所以采取的是假时间策略。
为了实现假时间策略,我将三部电梯的运行封闭到了一个线程当中,单独一个线程内部的执行是不会受到线程调度产生的误差的影响的。
在这基础上,考虑到输入IO会有阻塞,安排了一个输入线程。至于调度器线程,对于使用假时间策略的我的设计而言,它是可有可无的,毕竟实际的调度都是在电梯线程内部进行的,如果不在电梯线程内部进行,那么调度与执行之间就会产生线程调度导致的时间误差,会导致正确性问题。故而我的调度器线程虽然按照指导书要求而加上了,但它仅进行部分不会产生时间误差的调度功能。
在输入线程与调度器线程之间仅有指令需要传递,我才用了一个阻塞队列来实现指令队列。利用java库中自带的同步容器类保证线程安全。
在调度器线程与电梯线程之间,存在两类同步问题:
第一类同步问题,因为我采用的是电梯线程内部假时间的策略,所以很容易解决,只需要利用一把调度器线程与电梯线程共享的“运行锁”即可。电梯线程每次主循环开头获取锁,主循环末尾释放锁。当调度器线程希望停止电梯线程时,只需获取该锁即可。同时为了避免饥饿问题,这里我采用了公平锁,在只有两个线程争夺该锁的情况下,性能损失还是可以接受的。
第二类同步问题,因为当调度器线程访问电梯状态时,电梯线程必定停止,不可能更新自身状态,故而仅需确保电梯状态都能反映到内存中,而不是被缓存。故而我大量使用了volatile变量、原子变量实现轻量级的同步。
线程划分非常明细、简单:
首先考虑触发器线程组,它们之间共享的是被监控的文件,而这份线程安全性被委托给了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,但是这课的设计就不是以给学生成绩为核心的。
言尽于此,并不是抱怨,这就是我所见到的,我所想的。
标签:功能 processor 过程 答案 编写 消息 程序 微信群 运行
原文地址:https://www.cnblogs.com/supplient/p/8973141.html