301
社区成员
发帖
与我相关
我的任务
分享

Main函数中新建一个全局的管理器Manager, 其构造函数先启动电梯线程,再启动输入线程。输入线程通过调用官方包得到乘客信息,然后直接通过Manager分配并唤醒对应的电梯。随后电梯执行运行策略。电梯线程的结束条件为:输入线程结束且当前电梯没有乘客也没有请求。


第二次作业新增了reset需求,且输入并不会指定分配到特定电梯,因而需要一个调度器来分配需求完成电梯的调度。第二次主要需要改动的点有:


第三次作业新增了双轿舱电梯,为了与之前作业的架构大致相似,我的双轿舱电梯实际上是两个电梯的组合。只是原来的电梯是从1楼到11楼,现在的电梯是从1层到换乘层或者从换乘层到11层。新建一个双仓电梯,实际上就是通过其载客量,速度以及换乘层在对应位置新建两个普通电梯。在我看来,双轿舱电梯和普通电梯的本质区别是:普通电梯只会在RESET时产生新的需求,而双轿舱电梯只要在运行就可能产生新的需求。因而加入双轿舱电梯后,调度器的结束条件将变为:输入线程结束,没有电梯在reset,且所有双仓电梯既没有需求也没有乘客。
回顾三次作业,会发现管理器,调度器和电梯的架构是相对稳定的,电梯的运行策略也是稳定的。需要修改的是根据不同指标修改调度策略以及应对可能的电梯种类,数量的变化等,但是电梯的运行策略一般来说不会改变。因此对于未来的扩展,若是增减电梯,或者增加电梯的种类抑或改变电梯的属性等,在该架构下都通过改写构造函数或者继承的方式较为简单地实现。但是,我的架构中一个Manager只管理当前的数个电梯,若出现需要多栋楼协作的情况,我不仅需要每一栋楼内都有Manager管理楼内的电梯,还需要一个全局的Manager来管理全局信息。而如何处理不同级别的Manager以及调度器的协作也是一个需要考虑的问题
在hw5中,因为每个需求到来都会被及时分配,所以我没有实现存储请求的结构,也就不需要在Manager中对相关结构进行同步。,此外,在我的架构中,每个电梯既具有电梯的属性,又执行电梯线程的运行策略。因而对于电梯线程,需要同步。
synchronized (targetElevator) {
targetElevator.addRequest(request);
targetElevator.notify();// 分配给对应的电梯,然后电梯开始工作。
}
// 分配给对应电梯的时候,需要在同步块内notify;
电梯类中需要HashMap<Integer, ArrayList<PersonRequest>> requestByFloor存储各个楼层的请求,这可能会通过Elevator.addRequest写入, 或者通过Manager读取,所以也需要同步。
hw6的同步块设计与hw5区别不大,只是由于电梯reset后可能产生新的请求放入Manager, 因此Manager需要一个存储请求的结构,也需要对这个结构同步,因为多个电梯线程可能同时reset产生新的请求。
需要注意的是
sleep是线程类的方法,可在线程里直接调用sleep()
synchronized(ElevatorA){
wait()
}
而wait, notify是Object的方法,是对对象调用的。表示当前对象在被synchronized的这个实例的监视器上等待,或者notify在该监视器上等待的线程。
通过同步块和锁地设计,可以避免双轿舱电梯的冲突。
hw7新增换乘层,除了对等待队列同步,为了避免电梯冲突,要实现对换乘层的锁与同步。我的实现是对于让双轿舱电梯共享一个锁对象,该对象在构造时传入。当A在换乘层且B将要进入换乘层,B唤醒A让A离开。
// MyLock.java
当transferState == 0的意味着换乘层空闲
当transferState == 1的意味着A在换乘层
当transferState == 2的意味着B在换乘层
电梯要进入换乘层的时候要先通过setState把换乘层标记为自己占有才能进入,否则需要等待。
// setState
synchronized (monitor) {
while (transferState != 0) {
try {
monitor.wait(); //此时想尝试进入换乘层的电梯会被阻塞
}
// ....
}
if (transferState == 0) {
transferState = elevator.getAbFlag();
// ...
}
// ...
}
当离开换乘层的时候要把state设为空闲并且唤醒等待的电梯
//cleanState
synchronized (monitor) {
transferState = 0;
monitor.notifyAll();
}
第一次作业由于每个乘客分配的时候已经指定好了电梯,实际上并没有实现调度策略。
第二次作业和第三次作业的调度器都由Distributor的run()方法实现
调度器与Manager通过共享的等待队列通信。InputThread接收到输入,解析添加到Manager的等待队列,并且唤醒调度器。此时调度器会直接执行调度策略,尝试将当前时刻下等待队列的请求都分配出去。
Distributor逻辑为:
执行调度策略
检查队列中是否还有请求:
有请求:说明还有请求没有处理,在hw6,hw7中意味着电梯都在reset,需要wait()
没有请求:如果没有请求且不会产生新的请求,则调度器线程可以结束,否则wait()
因而,调度策略可以很容易地替换。
hw6中我的调度策略是以电梯的请求,乘客数,容量,速度等指标计算出一个分数,从中选择分数最高的电梯。为了调整参数,可以把该调度策略与随机策略在生成的随机数据上的表现进行比较。但是,由于我没有实现一个量化的指标,只是简单地通过减小最终结束时间来尝试最大化速度而没有考虑耗电量等问题,因此性能表现并不算好。此外,生成的随机数据也没有考虑到强测数据的特点。
hw7中,由于双轿舱电梯耗电量比一般电梯小,所以考虑耗电量显然十分重要。但是,由于时间紧迫,我仅仅在hw6的基础上针对双轿舱电梯简单地加了个bonus以表示更倾向于选择双轿舱电梯,结果也是不出乎意料地寄了。
经历这段时间,我发现多线程程序debug实在是太困难了。正是因此,线程安全与层次化架构设计更加重要。对于线程安全,以等待队列为例。既然我们需要频繁地读写等待队列,那么每次对这个ArrayList操作前手动synchronized(waitingQueues)不仅麻烦,更容易因为忘记而出错。因而一种更省心的方法便是将相关的操作方法都写成线程安全的,这样频繁调用的时候就不会忘记。同时这也是层次化设计的一部分,这样,当我们在Manager中对等待队列进行操作时,我们才能不关心其实现细节以及等待队列的线程安全问题,才能专注于处理管理器-调度器-电梯线程之间的线程问题。在作业中,我们应该让每一部分只负责自己的功能,电梯线程只执行运行策略,调度器线程只执行调度策略。这样一来,所有新增的请求只需要放入请求队列并且唤醒调度器就可以得到分配,这样我们在电梯reset,新增新请求时只需要添加而无需理会细节。而对于调度器需要使用到的信息,应该由管理器处理之后传递给调度器,这样我们就无需在调度器内对电梯信息进行太多处理。