摘要:本次电梯作业极大的丰富了我对多线程的认识和了解,通过三次作业,基本掌握了对于多线程任务处理的常规方法。

1、设计构造:

思路(三次作业共同的整体构造):

乘客的乘坐请求经由输入线程传给主调度器,主调度器根据请求的特点将请求分散传递到各个分支调度器,分支调度器再将请求进一步发送给电梯,让电梯据此完成任务。其中,分支调度器设置在不同的楼座或楼层,一层或一栋楼设置一个,可调度该处所有的电梯,并可在该层楼或该栋楼增加电梯;而主调度器则依据请求特点将请求分类后发送给各个分支调度器。可以看出,这是一个类似流水线的构造,需要将乘客的请求层层处理并传递。而且根据描述,输入、主调度器、分支调度器与电梯都应设置为线程,分别处理不同的任务以提升效率。故作业的第一个设计重点在于因此第一项任务在于各进程之间的协调与通信。

(1)线程间的通信与协调方法:生产消费者模型。
在输入线程与主调度器、主调度器与分支调度器、 分支调度器与电梯之间构造“托盘”类,分别用一阶托盘、二阶托盘和三阶托盘对其进行表示。乘客请求将在托盘处被存取,同时每个部分的托盘都只与和这个托盘直接联系到的线程进行交互。

配置完线程后,第二个设计重点为电梯的调度。在本作业的设计中,调度方法主要设置在了三阶托盘的位置,电梯仅负责从调度器获取自己的下一个终点目的地(比如电梯在2层时接到4至9楼的请求,终点目的地为9楼)并检查是否需要开关门捎带乘客。

(2)电梯的调度方法:类似于Look策略的算法。
对于垂直电梯:
当电梯等待时(内部无乘客且没有目标楼层),选择可以接到乘客并且出发或抵达楼层距离自己最远的请求,将其中的最远楼层定为目标楼层发送给电梯。例如,电梯在2楼时,三阶托盘中有1楼到10楼、4到8和10到1的请求,从左到右三者目标楼层分别为1、8、10,与电梯的距离分别为1、2和8,故选择目标楼层为10楼。

在电梯运行时(目标楼层存在):在每层检查托盘中是否新增了与现在方向一致、乘客可以接到且距离更远的请求,如果有就更新目标楼层。同时在每一层检查是否有在该层上电梯且请求方向与电梯方向一致的乘客,有则将该乘客接上电梯。

在电梯抵达目标楼层时,如果不需要如上一条所述更新目标楼层,更换运行方向,重新更新最远目标楼层。由于每趟运行都将抵达同方向最远到的楼层,到达目标层时,梯内乘客将全部下电梯,故电梯此时选择目标楼层依旧仅需要三阶托盘中待乘电梯者的信息,不需要获取乘客的信息。

以电梯在6楼等待时为例,接到请求10到2后前往10层,抵达后由于该乘客还未乘梯(即10到2存在于三阶托盘的容器中),再次读取请求10到2,电梯更换方向并重新确定终点为1。此时,电梯运行方向为下行且处在10层,符合乘客的乘梯要求,开门接该乘客乘梯。
对于水平电梯:

与垂直电梯不同,水平电梯头尾相接,故简单修改垂直电梯的策略即可很好的完成调度。

最后需要考虑的问题是控制所有线程的结束,在本次作业中采用生产者对托盘设置setEnd的方法结束线程。

(3)生产者对托盘设置setEnd的方法结束线程

当输入线程停止输入后,将一阶托盘中的end值设置为true,退出输入线程循环,输入线程结束。主调度器每次循环都会检查一阶托盘中end的值,如果end值为true且一阶托盘容器中不再含有任何请求,则可判定主调度器应当结束,对二阶托盘setEnd并退出循环,线程停止。分支调度器和电梯的退出都采用相同的方式,不过电梯退出时需要额外检查是否有乘客,需要等待所有乘客都离开电梯后再结束线程。

至此,电梯系统的基本设计已经完成

2、一些设计中的具体问题:

(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系
由于三次作业中的架构都与上述的设计构造相同,共享对象为一阶、二阶和三阶托盘,因此多线程安全性问题只会发生在各个托盘类中,需要对其中的共享数据上锁。每一次作业托盘中共享数据锁都选用了synchronized,但具体上锁的方法有所不同。第一次作业中,锁都上在了方法声明处,即给this上锁;第二次由于对托盘的访问次数提升较多,因此需要提升效率,修改为不对this上锁,而对特定的共享对象上锁,如请求容器。第三次作业又再度对this上锁以牺牲效率换取复杂度的下降。

(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
三次作业中均采用多级调度器设计,设置主调度器、分支调度器以完成不同的任务。分支调度器设置在不同的楼座或楼层,一层或一栋楼设置一个,可调度该处所有的电梯,并可在该层楼或该栋楼增加电梯;而主调度器则依据请求特点将请求分类后发送给各个分支调度器。在设计中,调度器也是线程,因此调度器与线程的交互就是线程间交互,采用生产者消费者模型。

(3)三次作业架构设计的逐步变化:

由于架构设计大同小异,故类图与线程协作图仅以第三次作业的图为例。

第一次作业由于只有垂直电梯,因此线程数量较少。建立了如上述架构的流水线系统,利用生产消费者模型进行线程间通信,电梯调度也直接用上述策略完成。实现过程与上述架构基本相同,故不再赘述。

本次作业线程架构图如下:

 

第二次作业加入了水平方向电梯,并且在同一栋楼或同一层处可能有多部电梯同时运行。

在线程上,我将主调度器与输入线程结合,减少了一级流水线(然而第三次作业证明这并不是合理的修改);

由于新增了增加电梯的请求,因此在分支调度器中加入电梯容器,使调度器可以在本层/座新增电梯并控制电梯运行;

因为第二次作业支持一个楼栋或楼层中有多部电梯运行,因此修改电梯和电梯调度算法,在每个电梯中加入待执行请求队列。电梯从三阶托盘获取目标楼层的同时将产生该目标层的请求从托盘中取出,加入待执行请求队列。电梯每次接乘客前再次访问托盘,取出可以在本层上电梯的请求到其待执行请求队列。接乘客时,乘客仅来自待执行请求队列。这样处理既可以避免出现多线程安全问题,又能够让多部电梯根据自身情况合理、自由地竞争乘客请求,避免了将请求轮流分发给电梯造成的不合理情况(例如1号电梯在7楼上升执行1到10楼的请求,2号电梯在4楼下降执行6到2楼的请求时,新进入请求一从1到9楼和请求二从10到2楼,根据先后顺序将请求一发送给电梯一而请求二发送给电梯二。然而电梯一适合执行请求二且电梯二适合执行请求一),可提升电梯的运行效率。

本次作业线程架构图如下:

 

第三次作业加入了换乘机制。在本次作业中,我重新引入了主调度器,并将一阶托盘设置为单例模式的控制器controller。

控制器会记录系统中的所有水平电梯的信息。当遇到跨楼座且跨楼层的请求时,控制器访问水平电梯表选择最合适该请求的水平电梯,将该请求拆解为垂直-水平-垂直请求(含垂直-水平和水平-垂直。跨楼座且跨楼层的请求可以拆解为垂直、水平、垂直请求或者水平、垂直、水平请求,由于初始系统中每个楼栋都有垂直电梯而水平电梯只有一层有,因此采用垂直、水平、垂直运行的拆解方法更合理。),使得拆解出来的水平请求可以用这个最合适的水平电梯进行运送。请求被拆解后以链表的形式保存在控制器中。当主调度器获取请求时,控制器从可访问的请求链表集合中取出某一个链表的首请求,将其发送给主调度器,并将该请求链表设置为不可访问。可以看出,请求经过处理后,主调度器获得的请求一定是水平或者垂直请求,从主调度器到电梯这部分系统的运行与第二次作业完全一致,具有非常好的移植性。唯一的不同点在于乘客下电梯之后需要向控制器汇报,控制器查看该乘客的请求链表是否完成,若完成则删去该链表,若未完成则将该链表重新设置为可访问,使主调度器能再度获取到该乘客的剩余请求。

本次作业线程架构图如下:

本次作业类图如下:

 

本次作业线程协调图如下:

 

分析三次作业可以看出,由于第一次作业就前瞻性的采用了线程式调度器的设计和多级流水线的架构,每一次作业都可以非常方便的在前一次作业的基础上进行迭代开发,可移植性非常好。这里就凸显
出了优秀架构的优越性。它让代码工程开发变得事半功倍,让更多的修改实现更为容易,让更多的需求可以轻松的植入。

3.分析程序的bug

第一次作业中,由于调度算法的设计不够到位,电梯有很多运行空转的情况,导致出现了大量的RTLE。bug修复时我参考look算法,设计出了之前所提到的类look算法,使得整体运行效率大大提升。同时,还遇到了在同一时间大量请求涌入后输出时间戳出现紊乱的问题,我加入了输出类并对输出方法上锁,让所有需要输出的线程共享该输出类就能解决此问题。

第二次作业中,再次出现了RTLE。经过分析,这次的问题在于引入单层、单栋有多部电梯后,未能协调好待执行请求队列中的请求访问,造成了电梯不访问待执行请求队列,而又因为该队列不为空造成的死循环情况,使得进程无法正常退出。修改调度算法让电梯等待时也会检查待执行请求队列后就成功将该问题解决了。

第三次作业中同样出现了RTLE问题。分析后发现在横向电梯的调度转向时出现了错误的判断,导致横向电梯在两个楼座间来回往复运动。通过修改调度算法增加判断横向电梯的转向条件即可解决问题。

总结:bug主要出现在了输出线程不安全与电梯调度算法错误两个方面

4.分析HACK所采用的策略
同样出于对hack模式缺乏兴趣及个人时间原因,本单元作业的互测中,我个人仅仅简单地参与了一下。
互测策略:

(1)暴力测试法:在要求范围内使用大量数据同时输入进行hack;

(2)线程安全性测试法:让多部电梯根据前序请求在某一时间点抵达同楼层,在该时间点对该处输入大量请求,检查是否出现线程安全性问题。

5.心得体会
(1)多线程程序要解决首要问题是线程安全问题。保障线程安全才能让各个线程的工作合理的展开。进行多线程安全性设计时,采用诸如生产者消费者模型的线程交互模式可以极大的方便线程安全性的
维护。当然语句原子化和lock锁等方法也是非常优秀的安全维护手段,只是在本次作业中,个人对synchronize锁使用更为熟练也更有把握,故安全维护都使用synchronize锁。
(2)工程设计中,优秀的架构就是成功的一般。好的架构就是一个结构稳固可靠、拓展方便的框架,放里面装东西、加东西都很方便,具有更好的逻辑性、可移植性和可操作性,对于后续的工程展开大有裨益。
(3)多线程的一大难点在于程序的调试与debug。我的调试方式是首先打开任务管理器,运行时查看任务管理器中IDEA对cpu的占用;其次是在每个线程的关键节点进行输出,查看线程的数据和运行状态