OO第二单元-多线程
作者:互联网
考虑到三次作业的迭代性,我将详细的文件结构和度量分析都放在了task3的部分里,前两次就简单略过了。
task1
初识多线程时的个人思考
线程涉及
- 获取输入的线程:
new Thread(new MyInput(sceduler))
- 调度器线程:
new Thread(scheduler)
- 每个电梯控制器的线程:
new Thread(ec)
- 在开关门时,主线程中又新建了线程:负责电梯的开关门
- 主线程负责控制:人的进出
提醒自己:为什么有的方法/代码块必须要加synchronized
?
同步(即带有synchronized
的)代码块/方法,其需要设置为同步,是因为,该方法/代码块所操作的数据有可能被不同的线程在同时操作。
如果不设置为同步,那就:
- 无法使用wait、notify,也就不得不让CPU轮询。
- 还可能引发读写问题、输出问题等。
想清楚这个,我们就可以:
- 只对必要的方法/代码块加同步限制
- 在同步方法/代码块的必要位置添加notify/noyifyAll
而非盲目的随意添加。
性能优化点
- 在电梯没有运送任务的时候,让它移动到程序手动设定的默认楼层。(例如,这10层有请求的频率相同,那么在没有客户来的时候,让电梯静静地待在1层不如静静地待在5层好)
- 当没有新任务时,此时无需让它在移动到默认楼层,线程直接结束。
注意点
-
无需考虑关门前是否所有想要进/出的人都已经完成了进/出,因为开关门的期间很长(0.4s),而capacity才为6人(又不是600000),肯定能够保证所有人在0.4s内都完成了进出
-
注:之后,在和同学讨论的过程中,我发现同学们大多都采用:开门 -> sleep0.4s -> 乘客进出 -> 关门,而不是像我一样一个进程开关门、一个进程让乘客进出。由于CPU在程序中占用的时间比例很小,所以前者的方法也并不会带来很大的性能损失,反而由于没有引入新的进程,会让整个算法更加清晰明了。
有趣的是,我在本次作业的bug也与这个点有关。详见后文。
UML类图
度量分析
bug分析
bug
本次强测、互测错了好几个点,都是因为电梯超速。我一开始百思不得其解,后来,注意到是在电梯开关门的时候有bug:
电梯在某层停下来时,我的实现方案是:
// ElevatorController类的方法
private void halt() {
Debug.println("halt!");
new Thread(() -> {
open();
close();
}).start();
// 先出
...
// 后进
...
synchronized (this) {
if (isPassable()) {
try {
Debug.println("正在等待着关门..");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
就是说,在halt()函数中:
- 电梯停下来后,单独开一个线程:按照开门时间、关门时间时间执行开门和关门,关门结束后notify
- halt()函数的线程里:进行乘客先进、后出的操作,然后等待关门(因为电梯处理的乘客进出的时间一定小于开关门时间,所以,此时一定会wait),等着关门时notify自己,然后halt()执行结束
其中,关门的函数如下:
// ElevatorController类的方法
private void close() {
elevator.close();
setPassable(false);
MyOutput.println(String.format(
"CLOSE-%c-%d-%d", getBlock().getBlockId(), curFloor, elevator.getId()));
Debug.println("电梯门close时,更新状态:");
updateState();
}
// ElevatorController类的方法
private synchronized void setPassable(boolean passable) {
this.passable = passable;
notifyAll();
}
bug就出在这里:
- 调用setPassable()关门后,母函数halt()结尾处的wait()完毕,然后halt()函数执行结束。
- 开始执行新一轮的run(),即开始上下移动(此时还没有输出CLOSE信息),导致下一次ARRIVE的时间和CLOSE信息之间差得不足moveTime(电梯超速),就会wa
事实上,应该先输出CLOSE信息,再进行上下移动,才是正确的。
bug修复
只需将setPassable()放在输出语句的后面。确保先输出CLOSE信息,再setPassable(),然后才notify,然后halt()才结束、开始接下来的新一轮run()
// ElevatorController类的方法
private void close() {
elevator.close();
MyOutput.println(String.format(
"CLOSE-%c-%d-%d", getBlock().getBlockId(), curFloor, elevator.getId()));
setPassable(false);
Debug.println("电梯门close时,更新状态:");
updateState();
}
task2
UML类图
度量分析
bug分析
bug
hack point:
[1.1]ADD-floor-512-1
[4.0]63-FROM-E-1-TO-D-1
[4.0]391-FROM-C-1-TO-A-1
[4.2]101-FROM-A-1-TO-B-1
[4.2]118-FROM-B-1-TO-E-1
我的部分错误输出:
[ 4.2490]ARRIVE-B-1-512
[ 4.4500]ARRIVE-C-1-512
[ 4.6510]ARRIVE-D-1-512
[ 4.8510]ARRIVE-E-1-512
[ 5.0520]ARRIVE-A-1-512
[ 5.0530]OPEN-A-1-512
[ 5.0570]IN-101-A-1-512
[ 5.4540]CLOSE-A-1-512
[ 5.6540]ARRIVE-B-1-512
[ 5.6550]OPEN-B-1-512
[ 5.6560]OUT-101-B-1-512
[ 6.0560]CLOSE-B-1-512
[ 6.2560]ARRIVE-C-1-512
[ 6.4570]ARRIVE-D-1-512
[ 6.6580]ARRIVE-E-1-512
[ 6.8580]ARRIVE-A-1-512
[ 7.0580]ARRIVE-B-1-512
[ 7.2590]ARRIVE-C-1-512
[ 7.4600]ARRIVE-D-1-512
...
产生的问题
- 从A座1层到B座1层后,更新电梯状态,由于在电梯右面的C座1层有人向上电梯,所以此电梯的状态仍为Rightwards
- 电梯状态为Rightwards,然而在B座1层有还在等电梯的118号想左行(
118-FROM-B-1-TO-E-1
),所以118号乘客无法进入电梯 - 从B座1层到C座1层后,更新电梯状态,由于在电梯右面的E座1层有人向上电梯,所以此电梯的状态仍为Rightwards
- 电梯状态为Rightwards,然而在C座1层有还在等电梯的391号想左行(
391-FROM-C-1-TO-A-1
),所以391号乘客无法进入电梯 - ...
这样,电梯就会陷入一直右行的状态,但是想要左行的在等电梯的人无法进入电梯,程序陷入死循环,118、391、63号Person根本无法进入电梯,最终TLE
究其原因,是因为,我在编写横向电梯的代码时,Person对象进入电梯的标准沿用了和纵向电梯一样的标准,为:电梯方向和请求方向一致才能进入电梯。没有考虑到横向电梯和纵向电梯还有一个很大的差别在于:
- 纵向电梯到最高层就会反向,不会出现所在楼层的上面一直有PersonRequest请求进入电梯
- 横向电梯的路线是循环的,所以可能出现所在楼座的右侧可能一直有PersonRequest请求进入电梯
bug修复
- 对于横向电梯的请求,放宽其进入电梯的标准。从原来的“电梯方向和请求方向一致才能进入电梯”变为“直接进入电梯”。
这回,我的程序输出:
[ 4.2430]ARRIVE-E-1-512
[ 4.2440]OPEN-E-1-512
[ 4.2490]IN-63-E-1-512
[ 4.6450]CLOSE-E-1-512
[ 4.8450]ARRIVE-D-1-512
[ 4.8450]OPEN-D-1-512
[ 4.8460]OUT-63-D-1-512
[ 5.2460]CLOSE-D-1-512
[ 5.4460]ARRIVE-C-1-512
[ 5.4470]OPEN-C-1-512
[ 5.4470]IN-391-C-1-512
[ 5.8470]CLOSE-C-1-512
[ 6.0470]ARRIVE-B-1-512
[ 6.0480]OPEN-B-1-512
[ 6.0480]IN-118-B-1-512
[ 6.4480]CLOSE-B-1-512
[ 6.6490]ARRIVE-A-1-512
[ 6.6490]OPEN-A-1-512
[ 6.6490]OUT-391-A-1-512
[ 6.6500]IN-101-A-1-512
[ 7.0500]CLOSE-A-1-512
[ 7.2510]ARRIVE-E-1-512
[ 7.2510]OPEN-E-1-512
[ 7.2520]OUT-118-E-1-512
[ 7.6520]CLOSE-E-1-512
[ 7.8520]ARRIVE-A-1-512
[ 8.0520]ARRIVE-B-1-512
[ 8.0530]OPEN-B-1-512
[ 8.0530]OUT-101-B-1-512
[ 8.4530]CLOSE-B-1-512
task3
UML类图
我的换乘策略
如图所示,有个人想p0->p3.那就需要先p0->p1
____________________________________
| | | | | |
| | |______| | |
| | |__p3__| | |
| | | | | |
|______| |______| | |
|__p1__| |__p2__| | | // 中转层:hecTsf所在层数(若hecTsf为null,则中转层为1)
| | | | | |
|______| | | | |
|__p01_| | | | |
| | | | | |
|______| | | | |
|__p0__| | | | |
| | | | | |
|______|______|______|______|______|
如果hecTsf不为null,则p1应该尽可能接近p3所在层(尽量一步到位,避免出现p0->p01之后,新的PersonRequest又进行p01->p1)
文件结构
度量分析
Class Metrics
Main类里的圈平均复杂度有些高。我认为原因是:我直接在Main.main()方法里通过for循环来实现了加入电梯。
MyInput类里的圈平均复杂度有些高。我认为原因是:出现了较多的if-else。这也无可非议,因为该类和Scheduler类的instance直接关联,只有通过条件判断才能将不同类型的指令(PersonRequest、横向ElevatorRequest、纵向ElevatorRequest)交给Scheduler的instance。
ElevatorController和HorElevatorController(两者相似,下面仅展示ElevatorController的类内方法)的类总圈复杂度有些高。我认为原因是:
- 类的方法太多啦!且个人感觉在这个开门、关门、进人、出人等等的一系列处理,有点“面向过程”了...这里面还出现了很多private方法。如果要针对面向过程进行优化的话,需要重构。只是,我当时就在写代码的时候,思路很清晰,就按照这种写法来写了。
Method Metrics
其中,ElevatorController和HorElevatorController的updateState()的iv(G)和v(G)和CogC较高。我认为原因是:
- 该方法的功能就是一个有限状态机,实现了电梯运行状态的切换。所以设计逻辑会较复杂。
- 调用了一些其他方法
- 方法行数多
- 我自己还做了一个“默认层”的优化,(详见task1的"性能优化点"一节),致使其更加复杂一些
此外,ElevatorController和HorElevatorController的run()的CogC和ev(G)较高。我认为原因是:
- 同样涉及到了和电梯状态有关的switch-case
- 调用了一些其他方法
bug分析
有一个强测点RTLE了。我猜测应该是自己的换乘思路比较简单。
我在本地测试了很多遍,都是比时间限制(155s)要短十秒以上。于是在bug修复时,我尝试将原版重新提交了一遍,又全部ac了。而且这次比时间限早了15秒左右。可见多线程的在竞争分配上的一些不同(比如第一次执行时A电梯抢到了a人,第二次执行时B电梯抢到了a人),就会导致很显著的差别。
总结
这一单元,相比多项式解析单元,代码量和复杂度会有所下降。但是,多线程本身带来的诸多问题却非常值得我们重视。
- 上课时了解到同一段代码每一次执行的结果都可能不同,会取决于操作系统对各个线程的时间片分配
- 对于共享数据的处理,要用同步代码块、加锁来保证线程安全(原子操作的概念)
- 刚开始做本单元第一次作业时,我在讨论区分享的关于“官方输出包线程不安全,会怎么样呢”的思考(虽然这里貌似没有“对于共享数据的读写”,但是我想,广义上来讲,输出控制台就是“共享数据”,每个线程都在向控制台输出,也就是在写入、改变共享数据)
- 第一次作业出现的仅仅由于两行代码的先后顺序反了,带来的“电梯超速”问题
- 多线程的调试不方便,解决办法是:建立Debug类,然后在适当地方加入Debug.printf()来调试
- ...
总之,这一单元的“玄学”bug比上一单元多了很多,究其原因就是多线程引发的CPU在调度、分配时间片上的问题。让我感叹有趣的同时,也从一个全新的视角领悟到:原来软件、程序和我们的硬件、操作系统的关联,是如此紧密。
标签:OO,多线程,512,电梯,线程,CLOSE,ARRIVE,bug,单元 来源: https://www.cnblogs.com/wlc000/p/16216052.html