OO第二次博客

 

oo5_7html

多线程同步策略分析

1.多线程电梯时的策略

线程分析

多线程电梯时,我还执着于时间的精准性,也就是上下楼必定要多少多少秒,因此采起的是假时间策略。java

为了实现假时间策略,我将三部电梯的运行封闭到了一个线程当中,单独一个线程内部的执行是不会受到线程调度产生的偏差的影响的。git

在这基础上,考虑到输入IO会有阻塞,安排了一个输入线程。至于调度器线程,对于使用假时间策略的个人设计而言,它是无关紧要的,毕竟实际的调度都是在电梯线程内部进行的,若是不在电梯线程内部进行,那么调度与执行之间就会产生线程调度致使的时间偏差,会致使正确性问题。故而个人调度器线程虽然按照指导书要求而加上了,但它仅进行部分不会产生时间偏差的调度功能。github

同步策略

在输入线程与调度器线程之间仅有指令须要传递,我才用了一个阻塞队列来实现指令队列。利用java库中自带的同步容器类保证线程安全。web

在调度器线程与电梯线程之间,存在两类同步问题:canvas

  1. 为了不因时间偏差而产生正确性错误,我须要在同一时间获取三部电梯的状态,而且能在该时间将指令传递给电梯线程。
  2. 我须要保证电梯的状态变动能在调度器线程须要访问的时候就能被反映出来。

第一类同步问题,由于我采用的是电梯线程内部假时间的策略,因此很容易解决,只须要利用一把调度器线程与电梯线程共享的“运行锁”便可。电梯线程每次主循环开头获取锁,主循环末尾释放锁。当调度器线程但愿中止电梯线程时,只需获取该锁便可。同时为了不饥饿问题,这里我采用了公平锁,在只有两个线程争夺该锁的状况下,性能损失仍是能够接受的。缓存

第二类同步问题,由于当调度器线程访问电梯状态时,电梯线程一定中止,不可能更新自身状态,故而仅需确保电梯状态都能反映到内存中,而不是被缓存。故而我大量使用了volatile变量、原子变量实现轻量级的同步。安全

2.IFTTT时的策略

线程分析

线程划分很是明细、简单:ruby

  1. 触发器线程组
  2. summary线程
  3. detail线程

同步策略

首先考虑触发器线程组,它们之间共享的是被监控的文件,而这份线程安全性被委托给了FileCenter这一线程安全的File类的封装类。服务器

再考虑summary线程、record线程,它们之间没有共享,但各自都和许多触发器线程共享了它们记录的信息。这是典型的“读者-写者”的状况。写者是一堆触发器线程,读者是须要将记录的信息写进文件的summary、record线程。我采用了消息队列的方法保证了写者与读者的同步,将线程安全性委托给了java的同步容器类。

3.第一次出租车时的策略

线程分析

这一次我抛弃了多线程电梯时注重正确性的策略,没有采用假时间策略。故而这里100个出租车再也不只有一个线程,而是真正的100个线程。

同时,我注意到对乘车请求的响应、“抢单窗口”的设计很是适合使用服务器模型进行实现。故而我安排了一个线程池,这个线程池中一个线程对应于正在处理的一个乘车请求,称该线程为调度单元线程。

除此以外,就还有一个标配的输入线程。

同步策略

首先,出租车之间共享地图,以及调度单元。

  1. 由于地图在此次做业中是不可变的,故而线程安全性被保证。
  2. 对于调度单元,我采用了消息机制来处理出租车、调度单元之间的同步问题。即二者都具备消息队列,互相传递信息时仅经过消息队列进行。这样虽然下降了性能、正确性,但简化了实现逻辑。

其次须要保证出租车的状态对其余线程均可见,这一点经过简单的内部锁便可实现。

最后由于须要经过位置、状态来访问出租车,故而我安排了一个缓冲用的TaxisMonitor,出租车监控类来存储缓存信息。由于该缓冲对象会被全部出租车访问来更新缓存,故而须要进行同步。这里我采起了细粒度加锁策略,毕竟自己就是为了性能而作的缓冲,不能由于加锁反而损失性能。对于每个位置上一个锁,每一种状态上一把锁。

不过由于每一次出租车状态更新会须要访问先后两个状态,若是同时获取两个状态的锁,会致使死锁问题。个人解决方法是让程序同一时间要么只获取前一状态的锁,要么只获取后一状态的锁,虽然会致使出租车在一段时间内在缓冲区中不可见,但能够简单解决了死锁问题。而不可见致使的正确性损失,在此次做业中并没有伤大雅。若是真的会由于这点时间的不可见而产生正确性错误,那出租车线程自己运行的时候就会由于过卡而致使走一条边超过200s了。

度量分析

电梯

此次电梯做业光看度量的面板数值还能够,也就输入处理那里我图省事嵌套多了点。可是实际上由于是屡次更迭的项目,其中有众多冗余的代码。这点从55个类、2898行代码中就能够看出。

IFTTT

从面板数值上能够看出,此次IFTTT做业最大的问题就是,它的分支判断至关地庞大。这一点我实在想不出怎么避免,我已经将分支判断尽量封装在一个方法中,而且保证该方法的接口统一性。或许能够采用将分支判断数据化,而后编写自动进行分支判断的代码来解决。

出租车

状态变化——这是此次出租车的红点。我在思考可否经过将状态自己也给抽象出来,做为一个类对待?而后状态变化的逻辑交由状态自己来处理?或许这样就能将复杂的圈逻辑降维,分散到各个状态类中去。

类分析

电梯

从LiftsThread以右下的那一部分代码所有都是和单线程时一致的,也就是电梯内部依然是按照单线程时的运做模式进行运做,再也不赘述其内部实现。

为了可以复用单线程时的代码,我在LiftsThread内部,将系统时间的流逝转化成了对Lift响应模拟时间变化的调用次数。也就是每过几几秒就调用一次Lift一个时间粒度的变化函数。

因此实际上,LiftsThread仅仅只是一个用来封装模拟时间的线程,其内部不包含任何调度逻辑。

实际的调度逻辑所有被包含在Schedular及SubSchedular中。其中SubSchedualr为单部电梯时的调用逻辑。Schedular为协调三部电梯的运动量均衡策略逻辑。

SchedularThread仅仅是为了迎合指导书要求而赘写的中介线程。

InputThread负责读入指令,并将其放入CommandTray中。以后SchedularThread从CommandTray中取出指令,再暂停LiftsThread,转交给它指令。

World负责系统内时间的管理。

IFTTT

ifttt中我大量使用了继承,主要缘由是指导书的不明确以及来自助教的需求的频繁变动,致使了代码须要不断维护。为了减小代码维护时的工做量,我尽量地复用代码,减小同质代码的出现。

继承树一共有四支。

  1. Trigger树。Trigger即为触发器,每个Trigger都是一个实现了Runnable的可运行类,在实际的程序运行中,每一个Trigger都是一个监控线程。其工做即为每隔一段时间获取监控对象的快照,随后根据自身响应方法的具体实现,生成Alter,交给注册在本身身上的Task处理。
  2. Task树。Task即为任务。Task负责接收文件快照的变化(Alter),随后根据自身响应方法的具体实现,进行处理。
  3. Recorder树。Recorder负责信息的记录与记录文件的读写。其自身也是一个线程,每隔一段时间就刷新文件中的记录。
  4. Test树。为了方便测试者进行测试,我将较为具体的测试时的运行逻辑封装在了抽象基类Test类中。每个具体实现了Test类的类均可以做为一个测试样例执行。

FileCenter即为一个线程安全的File类的封装类,负责文件读写的底层封装。

出租车

出租车的类设计主要分为了三个族:

  1. Taxi族。这部分包含了Taxi, Driver, TaxisMonitor。这一族内部高度耦合,三位一体地实现了出租车。

    1. TaxisMonitor负责出租车状态的对外查询,其内部实现为缓冲实现。每当出租车状态发生更新时就会调用该对象,进行缓冲更新。
    2. Taxi负责出租车的运动逻辑的维护。也就是出租车实际在地图上如何位移的问题。
    3. Driver负责出租车的服务逻辑的维护。也就是出租车如何与调度单元进行消息交互的问题。

    Timepasser为Taxi的基类,封装了Taxi的线程逻辑。负责将系统时间的流逝转化成Taxi内部时间的每时间粒度的流逝。

  2. Schedule族。这部分包含了ScheduleServer, ScheduleUnit。ScheduleServer维护了一个线程池,该线程池中每个线程都为ScheduleUnit的实例化。这一族负责响应乘车请求,并对每一个乘车请求分配一个线程进行抢单、分单等逻辑。

  3. Map族。这部分包含了Map, RouteSolver。这一族封装了地图的具体实现。

除了这三族之外,还有几个类是为了以后的扩展而额外实现的:

  • TwoSideMessage。给消息增长了一层抽象,使得以后还能扩展出除了乘车请求交互时的消息之外的消息。
  • Mesable。该接口是为了使得以后其余类也可能可以收发消息而实现的。
  • InputProcessor。该类将输入处理与乘车请求响应分离开来,是为了以后增长除了乘车请求的输入。

设计分析

关于这个,我难以进行分析……由于我不知道我有没有作到。究竟作到什么程度才能算是作到了某一项原则?我没有足够的经验去回答这个问题……我只能说我尽可能去作了。

bug分析

主要的bug来自于对指导书理解有误以及未及时看issue和微信群。其次的bug大可能是由于我没写正则去检查输入是否合法,只要输入错了,那我就挑正确的而后继续跑,跑不下去再跑错,可是公测要求无论能不能继续跑都要报错。

我没怎么查别人的bug,没那个时间,仅仅只是按照公测要求完成了课程安排的工做。

心得与体会

其实多线程的同步控制并不困难,与电梯复杂的调度逻辑相比,那是很简单的东西。代码编写过程当中最大的难点实际上是两点:

什么是正确的代码?

我很想说我知道这个问题的答案,但我作不到。仅仅只是阅读指导书,进行需求分析,并不能真正导向“正确的代码”,而只是能接近。在知道这件事情之后,我所能作的或许仅仅只是留出“将代码改正确的余地”。

性能 || 简单

在程序写完前我无从得知程序的性能如何——这很显然,可是我却必需要写完它。若是我在编写过程当中就考虑性能问题,那颇有可能就会致使我代码根本写不完,或者越写越错。由于优化性能的代码逻辑每每是复杂的。在第一次编写的过程当中我每每会采起最简单的那种方法,而不是最快的方法。我所能作的或许仅仅只是留出“将代码变快的余地”。

总结

上述两个问题最终都导向一点:余地。也就是代码的可修改空间、可拓展空间。当我编写的程序不仅是写完就行,过了OJ就行的时候,“余地”成了我编写代码时须要重点考虑的因素。

对课程的见解

在电梯的时候,我十分固执于程序的正确性,经过不断地发issue明确指导书的意思,我虽然不能彻底作到,可是却能向“正确的程序”接近。

不过在ifttt时,需求频繁的变动、指导书的不明确让我放弃了对正确性的固执——那是一件作不到的事情了,一样一份代码,两我的看,一我的以为对,另外一个以为错,再也不有统一规定。

公测逐渐转为互测,互测时bug树能够用一个bug挂满,许多人的公测由于对方没测而满分,吐槽版各类认领代码,等等现象,让我明白了一件事情:这课的成绩没有很大的意义,就和个人博雅课程的成绩的意义差很少。虽然这课的成绩会算进GPA,可是这课的设计就不是以给学生成绩为核心的。

言尽于此,并非抱怨,这就是我所见到的,我所想的。

相关文章
相关标签/搜索