OO_BLOG_UNIT2
Unit2_OO_Blog
1. 第一次作业
1.1 同步块的设置和锁的选择
- 本次架构设计主要借鉴了第三次课上实验的代码设计。设置了读入请求线程、调度线程、电梯线程三个线程。
- 本单元同步块设置主要针对整体的等待队列以及电梯内的乘客队列。在乘客分配时使调度线程持有等待队列、电梯乘客队列的锁;在乘客进出电梯时使调度线程(本次设计中调度线程可以控制乘客进入电梯)或电梯线程持有对电梯内乘客队列的锁。
1.2 调度器
三次作业均将调度划分为乘客的分配调度、电梯的运行调度两部分。
但由于第一次作业的架构设计非常之烂,以下主要从三种Pattern的角度进行调度分析。
-
对于本次只有一个电梯的设定,本次没有设置电梯的等待队列。调度类直接向电梯内的乘客队列分配乘客,并且调用了电梯线程中乘客进入电梯的方法。(本次设计的架构缺陷之一)
-
Random模式
-
调度线程:采用ALS可捎带原则分配乘客。
电梯为空时,调度线程分配最早进入等待队列的请求;电梯不为空时,分配可捎带的请求。除主请求之外,调度线程通过调用电梯线程方法控制乘客进入电梯。
-
电梯线程:采用主请求的核心思想。
以主请求确定当前的
destination
以及direction
,从而控制电梯开关以及通过更换主请求控制电梯运行方向。
-
-
Morning和Night模式
- 调度线程:在所有请求读入完毕时开始分配,每次分配至乘客人数达到电梯容量上限。
- 电梯线程:
- Night模式下在等待队列中找到出发层最高的为上升时主请求。
- Morning模式固定了目的地,因此下行时无需主请求,上行时与Night一致。
1.3 分析程序bug
- 本次作业在强测中不出意料的大翻车,多个点tle。因为我发现架构设计的漏洞很难修改,也很难支持后续作业的完成,因此经过一个礼拜对于多线程相关知识的学习以及对书籍的阅读,我决定重新设计架构,最终利用第二次作业架构修复了第一次作业的bug。
1.4 心得体会
本次架构主要出现了以下两个问题:
-
第一个问题:调度类耦合了电梯线程的功能
-
第二个问题:同时在实现时由于对于synchronize()以及notifyall()理解并不到位,因此最终实现的电梯几乎是一个单线程的电梯。
一方面架构设计的非常糟糕,另一方面对于多线程同步块的设置以及锁的选择在此时非常混乱。
2. 第二次作业
2.1 同步块的设置和锁的选择
- 由于明确了线程间共享对象的交互问题,此次作业的锁的选择方面在实现中并无bug。
![pic1](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214307202-334051985.png)
- 一方面,调度线程访问电梯线程必要的属性(当前状态、当前等待队列的乘客数)实现目标电梯的选择,并通过持有目标电梯等待队列的锁实现乘客分配;另一方面,通过整体的等待队列再无乘客以及全部分配结束的信息,控制电梯线程等待队列的
nomore
属性。 - 电梯线程通过判断当前电梯对应的等待队列是否有乘客、无乘客时是否还会有乘客、电梯内是否有乘客,从而控制线程,核心是对eleWaitQueue持有锁,避免与调度线程同时对该对象进行写操作。
synchronized (eleWaitQueue) {
if (eleWaitQueue.isEmpty() && eleOutQueue.isEmpty() &&!eleWaitQueue.nomore()) {
// wait for PersonRequest
try {
eleWaitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (eleWaitQueue.isEmpty() && eleWaitQueue.nomore() && eleOutQueue.isEmpty()) {
// stop
return;
}
eleWaitQueue.notifyAll();
}
2.2 架构分析
- 本次重构的架构设计主要借鉴了研讨课同学分享的想法以及第一次作业的一些处理。由于第一次作业大致的框架可以支持多部电梯的运行,因此对读入线程及调度线程启动电梯线程的设计予以保留,而对调度线程以及电梯线程的设计进行了较大的改动。最终实现的核心变化为以下两点:
- 将pattern类从调度线程剥离,完全内置于电梯线程。
- 电梯线程内采用状态机处理请求的核心思想。
![pic2](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214631587-1584043284.png)
-
以下对调度线程以及电梯线程的设计进行分析(UML类图如上):
-
ScheduleThread
- 功能:
- 从InputThread中获取请求,依赖于共享对象WaitQueue
- 从WaitQueue中将PersonRequest分配请求给ElevatorThread的等待队列eleWaitQueue.
- 采用优先选择可捎带当前请求的电梯的乘客分配策略。
- 功能:
-
ElevatorThread
-
功能:
- 根据不同的pattern,实现对电梯内等待队列eleWaitQueue中的请求处理。
-
采用状态机思想进行电梯运行调度
- 状态分为上行UP、下行DOWN、等待WAIT三种。
- 初始化状态为WAIT;等待队列有请求时更改状态为UP或DOWN,此时采用Look算法进行处理;当电梯内再无处理请求时转换为WAIT,同向请求处理结束且反向有请求,或反向有乘客请求时状态转移为与当前运行方向相反的状态。具体的状态转移图如下:
-
-
![pic3](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214341786-1621661274.png)
2.3 分析程序bug
- 弱测环节无bug,强测环节两个卡时间的边界测试数据tle,互测屋风平浪静(目测大家都无心hack了)。
-
debug:
主要原因是调度线程中的分配策略导致了电梯资源的浪费,简单的将每次从第一个电梯判断是否可捎带,改为下一次接着上一次问询的电梯开始问询,时间减少了,不过囿于本身这种分配思想的局限,仍旧会tle,此处计划通过下次作业对于调度类策略的重新设计改掉bug。(最后悲伤失败,详见下文aww)
-
2.4 心得体会
- 本次作业完成中,鉴于自己对重构的架构设计思路比较清晰,所以上手写的过程相对比较顺利。
- 在实现过程中,因为一个20行左右的方法内部的两处笔误,导致周末debug找了很久。而在第三次作业试图更换调度策略并且顺手重构的时候,发现了本次作业存在着很多并不合理的方法设计,比如方法调用层数太多,实现中有大量冗杂可以简化甚至删除的方法、电梯线程内部存在着可以剥离或规范化为类的成员等等问题。
- 如下图对各队列的存储处理的分析中,电梯等待队列以及电梯内请求出电梯的请求队列存在着较明显的重复。尽管两个类中的方法有一些差异,但是基础的方法并无区别。此处实际可以通过建立抽象类或者父类的方法简化。
![pic4](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214413639-681576261.png)
- 在二次重构的时候反思了原因主要是:设计架构的时候我主要关注了各个线程所需实现的功能、类所需的属性以及各个线程间对于共享对象持有锁及释放锁的处理,几乎完全忽略了对于方法设计的考虑,而一边写一边加方法的做法不仅使得方法命名不统一,更是使得类变得冗长,同时也增大了因为耦合而产生严重bug的可能。
3. 第三次作业
3.1 同步块的设置和锁的选择
- 由于本次架构与第二次作业几乎完全一致,仅有的区别在于调度线程。
- 本次作业在分配乘客时主要考虑因素为电梯类型,具体来讲就是根据相应的目标电梯类型选择最优电梯。在选择最优电梯时,调度线程需要读取电梯线程当前的运行状态,此时通过分配使调度线程持有目标电梯的锁,完成电梯选择。在电梯选择结束后,通过使调度线程持有该电梯等待队列的锁,实现乘客分配。
3.2 调度器
- 乘客分配调度
- 调度类
SchedulerThread
的实现遵循不换乘、按照类型分配的原则 - 按照C类、B类、A类的优先次序选择电梯,在同类别电梯中优先选择可捎带该乘客的电梯。即可被C类电梯运送的分配给C,否则考虑分配给B,另外两种均不可时分配给A类电梯。
- 调度类
- 电梯运行调度
- 采用状态机的核心思想。电梯运行控制的策略与第二次作业完全一致,在此不赘述。
ElevatorThread
电梯线程通过读入的电梯类型type初始化信息,以及每一次进入while(true)
时的电梯移动次数,从而调整不同类型电梯可停靠楼层。
3.3 架构设计可扩展性
- 本次作业最终上交版本的设计框架完全基于第二次作业,因此架构设计与第二次作业完全一致。架构分析完全同第二次作业,在这里直接提供UML类图及UML时序图如下。
![pic5](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214548067-1185724261.png)
![pic6](https://img2020.cnblogs.com/blog/2197936/202104/2197936-20210423214517982-1142590555.png)
- 对与调度线程而言,由于与电梯线程共享的对象主要是电梯的状态以及电梯等待队列二者,因此在更换策略时基本不需要修改电梯线程的设计。
- 对于电梯线程而言,其核心功能目前为电梯的控制(开关门、上下人)以及电梯运行方向的选择(状态机思想)。为进一步提升可扩展性,实际上可以给每一个电梯配有一个调度器,并将电梯运行方向的选择这一功能剥离在相应的调度器内。
3.4 分析程序bug
- 本次作业在强测以及互测中均没有被测出bug,互测屋无仍旧风平浪静(耶)。互测阶段没有找别人的bug。
- 但实际上如果有较多边界数据,如大量偶数层到偶数层的乘客等,其实很容易因为运行时间太长而TLE。
3.5 心得体会
-
显然,这一单元作业的最终提交版本非常摸鱼,实际上从新建文件到交上去过了测试只花了一个小时左右。但实际上我花了将近一天重新设计了调度策略,对于第二次作业中存在的不合理方法进行了删改,并且至少是完整的写完了。
- 新的调度策略主要借鉴于几篇助教的博客,采用了自由竞争的想法。主要想法为以下几点。
- 固定换乘站为1、7、15。
- 预处理乘客请求,判断其是否有换乘站以及换乘站的层数。
- 调度类“无差别”分配乘客请求,将每一个乘客请求分配给每一个可以运送该请求的电梯。
- 新的调度策略主要借鉴于几篇助教的博客,采用了自由竞争的想法。主要想法为以下几点。
-
但是,问题出现在了这里,由于我对线程安全理解的不深刻,导致debug火葬场。代码中频繁出现某一个电梯不能正常停止、难以复现的死锁的问题。调试修改过的bug包括但不限于:电梯线程与调度线程对于共享对象持有锁以及释放锁的时间的问题。一个白天加一个晚上debug使我痛不欲生,于是最后不得不选择了真香不换乘电梯。
-
新的分配策略对于线程安全的要求更高,但是我对于调度线程以及电梯线程共享对象的理解与处理并不清晰,一度混淆对电梯对象elevator加锁、导致后期其实越改越复杂,旧的死锁的问题根本没有解决而是制造了更多的死锁问题。
4. 结束语
-
总的来说,第二单元多线程的学习让我感受到了迭代开发所带来的好处,相比于第一单元痛苦面具多次重构,本单元的重构与迭代更加体现了学习的过程。虽然我的调度的算法并没有任何新颖之处,但是对于整体架构设计的体验还是很不错的,体会到了由设计到实现,再到总结问题、进行优化改进的过程。
-
好啦,和多线程暂时告别啦~