OO Unit2单元总结
OO Unit2博客
林世瀚 20185635
2022.5.4
1 同步块的设置和锁的选择,锁与同步块中处理语句之间的关系
锁的选择
-
对乘客等待列表加锁,防止读写冲突
private final ArrayList<Vector<PrCommand>> waitingList; synchronized (waitingList) { } synchronized (waitingList.get(i)) { }
waitingList
存储每座每层正在等待的乘客,是连接乘客请求输入与乘客请求处理的桥梁,输入进程和电梯控制进程同时读写。进一步地,由于每次只会读写一座、一层的等待列表,为了提升效率,可以把加锁的部分进一步缩小到waitingList.get(i)
。 -
对
RequestIn
类的实例加锁,使用wait-notify方法/*in RequestIn.java*/ public synchronized void addRequest(PrCommand pr);/*内含NotifyAll()*/ public synchronized void setEnd();/*内含NotifyAll()*/ /*in ElevatorCommand.java*/ synchronized (requestIn) { //这块 try { requestIn.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } /*in RequestCollection.java*/ public void endInput() { for (RequestIn reqIn : PRODUCERS) { synchronized (reqIn) { reqIn.setEnd(); } } for (RequestIn reqIn : PRODUCERS_LEVEL) { synchronized (reqIn) { reqIn.setEnd(); } } /*......*/ }
对于某个电梯,已经完成之前所有请求、等待列表又为空时,需要阻塞线程,防止轮询;当有新请求进入,或者得到输入终止命令后,又需要重新唤醒。考虑到
RequestIn
类的逻辑上位于中间的位置,这组wait-notifyAll
通过对RequestIn
类的实例加锁实现。(
RequestIn
类在程序运行逻辑上位于负责输入的RequestCollection
类与控制电梯的ElevatorCommand
类之间,经RequestionCollection
将乘客请求分发后,把乘客请求存入对应的waitingList
中。) -
对
RequestCollection
类加锁,防止读写冲突private static int SEM = 0; public static synchronized int getSem(); public static synchronized void setSem(int sem);
为了便于判断已输入的所有乘客请求是否全部完成运送到终点,在
RequestCollection
类中添加全局变量SEM
,新输入一个请求则+1,而一个请求运送完成则减,有点类似信号量。因为不同电梯对应不同线程都会修改这个变量,需要对其加锁。 -
对
SecureTimableOutput
类加锁,保证输出线程安全public static synchronized void println(String str);
锁与同步块中处理语句的关系
从同步块中调用的多线程方法来看,wait()
等方法的”主语“必须是同步块所对应的加锁对象。
从同步块中受保护的变量来看,加锁方式是多样的,锁与同步块中处理语句所涉及的变量并不存在绝对的关系。但为了保证线程安全,一般来讲,对于多个线程都需要修改的变量,只能出现在同一个锁所控制的语句中。
2 三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
三次作业中的调度器设计
在结构设计层面,三次作业相同,均采取自由竞争策略,对每个电梯进行单独的调度,而不使用集中调度器。
在策略层面,主要的三种ALS
、sstf
、look
都有尝试。对于题目给出的基准策略ALS
,整体性能一般,但比较可靠,同时使用时最好对捎带策略做出改进;对于最小寻道时间算法sstf
,平均水平最好但下界情况较差,可能会出现超时的情况;对于最常用的look
算法,整体水平最优,同时也是与生活中真实的电梯比较相像的。
策略性能比较是很大的难题。在无法分辨策略性能疏优疏略的前提下,使用模拟现实电梯的look
不失为一种好的选择。使用评测机生成数据,在实践中进行比较也是可行的。
没有使用集中调度器的原因是考虑到题目没有给出乘客请求出现的大体规律,不看好集中调度在性能上能有更优表现。如老师上课所说,“只要不知道下一时刻请求的时间出现情况,集中调度的策略就永远没有最优解”,在不能保证给定输入规律的前提下,即使策略是实时计算的,我们也只能保证“贪心”的结果在当前较优,而无法判断在下一时刻时是最优、还是很差。现实中写字楼的电梯调度往往有掌握了乘客出现规律的基础,类似去年的作业,分为早高峰、晚高峰等情况。本次作业删除了这些前置条件,相比之下不如使用自由竞争。
比较奇怪的一点,在我使用look
算法时,部分数据点的性能仍然比同使用look
算法的同学差很多。目前还没搞清楚是我对look
算法理解有误进而实现有误所引起的,还是其他细枝末节的地方所引起的。还有待研究。
调度器与程序中线程进行交互
/* in ElevatorCommand.java*/
private final ArrayList<Vector<PrCommand>> waitingList;
private final Vector<PrCommand> passengerList;
private int dest = 0;
public void run();
private int updateDestination();
将调度部分封装入updateDestination()
中,该方法读取waitingList
和passengerList
中的内容,并依据此修改、更新dest
,设置电梯当前运行的目的地。可以说,调度器与线程的交互是主要通过以上三个变量完成的。
这样的写法保证了较低的耦合性,当需要更改电梯调度策略时,只需修改updateDestination
方法,而不需要考虑程序中的其他部分。
3 架构模式与协作关系
UML类图
UML协作图
starUML过期了,目前还在研究破解,破解之后补上
架构分析
即使对于最后一次作业,主要架构仍然是“生产者-消费者”模型。获取新请求的类RequestCollection
是生产者,处理请求的类ElevatorCommand
是消费者,RequestIn
类则夹在中间分发请求,waitingList
变量则是中间的缓存器。总体而言比较简单清晰。
对于一个乘客请求需要分为好几段换乘的情况,我将PersonRequest
进行进一步封装,成为PRCommand
类,直接对外展示当前段的起终点;在每段结束后,判断请求的所有段是否均已经完成,若为否则更新PRCommand
对外展示的起终点,将其改为下一段的起终点。通过这样的设计替代了流水线架构。
未来扩展能力
更换调度策略
4 分析自己程序的BUG
U2-1
在处理电梯上人的ElevatorCommand.java/pushPeople()
方法中,对于电梯内已有乘客的捎带情况,理应只允许waitingList
中与电梯内乘客方向相同的请求进入电梯,但我却缺少了这层判断,让方向相同的、相反的全部进入了电梯,且并没有优先同向请求,很多情况下极大降低了电梯的运行效率。导致强测4个点超时RTLE
;而互测由于时间限制宽松,没有被成功hack到。
U2-2
未发现bug。
U2-3
-
电梯可达性方面,仅在规划路径时考虑到了可达性问题,而在电梯调度、上下人等方面忽略了,因此一旦同一层存在两台或以上可达性不同的电梯,就有可能出现在不可停靠楼层开门的问题。报错
WrongAnswer
。 -
路径规划方面,对于起终点在同一座或同一层的请求,直接归为可直达处理。前者没有问题,但后者忽略了可直达但该层无可用横向电梯的情况,会导致有请求一直在列表等待而得不到执行,从而程序无法结束,报错
RTLE
。 -
线程安全方面,前文1.3中提到的信号量
SEM
会被多个线程修改,但我一方面没有在setSEM
和getSEM
方法上添加synchronized
关键字,另一方面RequestCollection
中有一处对SEM的修改没有使用封装的方法。 -
不知原因超时一个点。
强测错误8个点,互测在b房被成功hack4次,我推测大部分错误是由前两点造成的,第三点虽然是线程不安全,但由于SEM
被调用到的次数就很少,触发的概率很小。
5 发现Bug策略
测试策略及有效性
前两次作业直接Hack线程不安全问题,在B房成功率较高。
发现线程不安全的策略
理清程序逻辑,以多个线程的冲突变量作为切入点检查;
多造数据,进行测试。
差异之处
Bug更隐蔽。
评测机
- JudgeStruct.py
- DataCreate.py
- FileParse.py
- DataJudge.py
DataCreate.py
用于生成input数据stdin.txt
。
FileParse.py
解析stdin.txt
和out.txt
,提取信息。
DataJudge.py
根据解析后的输入、输出,使用状态机模拟整个电梯运行过程,并评判正确性。
JudgeStruct.py
相当于main
函数。使用os.system
运行.jar
文件。使用_thread
可多线程同时评测多个程序。可实现多个文件、多轮运行的自动评测。
相比之下,写一个程序的评测机像是写程序本身的逆过程,如果搞清了原理,并不会太难,甚至还省去了架构设计的步骤。在实现上,难点更多的是在入手python,熟悉语法和库。从这个角度来看,评测机也确实是非常高效的。
6 心得体会
线程安全:这次作业我的程序总体而言是线程安全的。要保证线程安全可以从规避掉线程不安全的因素入手,首先要梳理清楚会使线程不安全的因素,如冲突变量等;在此基础上,想清如何加锁才能使线程变安全。
层次化:层次化方面我的程序还不够清晰。一方面是可读性较差,虽然我自己清楚是怎么回事,但换个人未必能懂;另外可拓展性也较差。内在原因是我对典型架构理解还不深,从客观角度上看,设计出来的架构必然只能是建立在我比较有限的知识上的,因此光自己“想”绞尽脑汁也设计不出更好的架构也是可以理解的。之后可以去重点学习一下有关的代码。
其他:这次作业代码量虽然和第一次差不多,但处理的场景更复杂了,对于某个方法或变量的功能,容易存在写时思路清晰但一段时间之后理解就变得模糊不清了的情况。从这个角度来讲,引出第三单元的内容就是很自然的了。