301
社区成员
发帖
与我相关
我的任务
分享
处理多线程的部分参考了实验代码。由于没有专门设计输入线程,MainClass在驱动程序的同时还负责输入处理:建立一个请求池RequestPool用于对输入进行缓冲、初始化6部电梯并为每一部电梯配备一台处理器(Controller)以及一个请求队列(RequestCenter)。Schedule是调度线程,在本次作业中它只负责把请求池中的请求按照电梯的编号分到相应电梯的请求队列中。Order是枚举类,存储了电梯所有可能接收到的指令。电梯线程Elevator会向策略类Controller类传入自己的状态来询问自己接下应该做的动作。策略类Controller会根据电梯传入的状态按照Look算法计算并返回电梯下一步的动作。

本次作业的请求中规定了乘客只能乘坐指定的电梯,调度器只负责将乘客送到指定电梯的请求队列中。在我的架构中调度器只会与请求池以及各个电梯的请求队列交互,因此并不存在与其他线程交互。
给对象上锁是为了处理共享对象的读写冲突。本次作业的共享对象有请求池(主线程写调度线程读写)、电梯的请求队列(调度线程写电梯线程读写)。因此只对这两种对象的方法上锁。
本次作业没有没有涉及线程通讯,因此bug很少。唯一的bug就是Look算法无法处理某些情景下电梯的转向。

和第五次作业相比增加了ElevatorState和Simulator。枚举类ElevatorState表示电梯的状态,这个状态主要是交给调度器判断电梯是否正处于重置的过程中。而Simulator类负责获取电梯状态并模拟电梯运行,得到当前电梯完成所有其请求所需要的时间。同时,本次作业Elevator类中也增加了大量代码用于处理Reset操作以及将电梯的当前状态传給调度器来模拟运行;调度器的调度策略也复杂很多:调度器每从请求队列中取出一个请求后都会对该请求进行模拟,并将请求加入运行时间最短的电梯的请求队列中,此时输出Receive。

采用影子电梯的调度策略。调度线程从请求池中取到一个请求后会从每部电梯中取得它们的状态并生成模拟器Simulator,并将当前指令加入到模拟器中模拟完成所有已有请求所需的时间。模拟器的实现方式和Elevator类基本一样,每一个模拟器都配备一个处理器Controller,它和电梯类的唯一区别就是上下楼和开关门不会消耗系统时间,但是会将本该消耗的系统时间累加并返回。调度器会将当前请求放入模拟时间最短电调的请求队列中。比较特殊的一点在于,模拟器无法模拟正在处于Reset状态的电梯,因为处于重置中的电梯会涉及到比较复杂的读写操作,这个时候获取到的状态是不准确的,可能会导致模拟出现错误(比如影子电梯到达0或是12层)。若是在调度的过程中遇到所有电梯都处于Reset状态,我会让调度器sleep(5)后再进行判断;在调度模拟的过程中若是某一部电梯处于重置状态,调度器则会直接跳过该电梯,不对其进行模拟,这会导致某些情境下模拟出的结果并不是最优的,会对性能产生一定的影响但并不大。
#如果某电梯在重置则不对其进行模拟
for (int i = 0; i < 6; i++) {
if (elevators.get(i).elevatorState() != ElevatorState.RESETING) {
Simulator simulator = elevators.get(i).ElevatorClone(request);
costs.add(simulator.run());
simulators.add(simulator);
customerNums.add(simulator.getCustomerNum());
}
}
#如果所有电梯都在重置
while (allReset(elevators)) {
try {
sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这次作业出现了很多的线程间通信:1. 某电梯Reset后其电梯中的请求以及请求队列中的请求要重新流回请求池中并进行重新分配,涉及电梯线程与调度线程的通信。 2. 调度线程需要从电梯线程中获取电梯当前的状态并生成模拟器,这也涉及电梯线程与调度线程间的通信。3. 在我的架构中,Reset请求是这样处理的:main方法识别reset类型请求 -> 将对应电梯的reset标志位置为1 -> 电梯发现reset标志位为1并开始重置操作。由此可见,这一过程中还涉及到了主线程与电梯线程之间的通信。为了保证信息安全,我给Elevator类中几乎每一个方法都上了锁,除了负责电梯移动的moving()方法,因为它不会涉及改变共享对象状态的操作。除此之外,我发现如果给moving()上锁会导致超时,因为电梯绝大多数时间都在楼层间运动,我给moving()方法上锁的话,如果此时需要获取该电梯的状态,则需要等待电梯运行结束,这会浪费很多的时间。
#电梯运行的方法
public void moving() {
try {
sleep((long) (speed * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (direction == UP) {
curFloor++;
} else {
curFloor--;
}
Functions.printMessage(ARRIVE, curFloor, elevatorID);
lastOperateTime = System.currentTimeMillis();
}
#用于获取电梯状态的方法
public synchronized Simulator ElevatorClone(CustomerRequest customerRequest);
本次作业虽然在公测和互测过程中没有遇到什么bug,但在写作业的过程中受到了很多bug的困扰。
在第五次作业中,当请求池中的所有请求被执行完后,调度线程就会自动结束。但在第六次作业中,即便是请求池中所有的请求得到了分配,还会有请求因为重置操作导致请求流回请求池。为了解决这一问题,我为调度线程增加了终止条件:我在请求池中设置了一个变量customersLeft,每当一个请求从调度线程流入电梯线程后执行customersLeft++指令;每当一部电梯完成一个请求后执行customersLeft--。
#调度线程的结束条件:
pool.isEmpty() && pool.isEnd() && pool.noCustomersLeft()
#由于变量customersLeft被多个线程共享
public synchronized void finishOneRequest() {
customersLeft--;
notifyAll();
}
public synchronized boolean noCustomersLeft() {
if (customersLeft == 0) {
notifyAll();
return true;
} else {
notifyAll();
return false;
}
}
#使用带锁的方法对其进行读写操作
本单元作业实现过程中经常会用到while (true)这样的语句块,若是不在语句块中进行适当的阻塞,一定会出现轮询的现象,占用大量CPU时长。本次作业中我的调度线程出现了轮询现象,是调度线程的终止条件没有写好导致的。
这种现象是概率触发的。若Reset请求和正常请求同时输入(但在输入文件中Reset要排在正常请求之前),有时正常请求的处理速度会更快一点,导致RECEIVE先于RESET输出,我没有想出很好的解决方法。为了避免出现这种错误,我选择“打补丁”。
#补丁
reset();
try {
sleep(10); //防止和reset同时计算的指令ARRIVE
} catch (InterruptedException e) {
e.printStackTrace();
}
在识别到Reset操作之后将线程阻塞一段时间即可。

与第六次作业相比多出了SharedFloor类防止双轿厢碰撞(将在3.3说明)。Elevator MainClass 和 Simulator中增加代码用来处理双轿厢重置与运行问题。为了使电梯能够“分裂”为双轿厢,我在最开始就初始化了12个线程,其中6个在wait(),等收到双轿厢重置指令后等待的电梯会被唤醒。双轿厢重置的读入和处理与普通的重置基本一样。


在上次作业的基础上实现了模拟解决双轿厢问题:将接到客人的轿厢把需要换成的客人送到换乘层所需的时间加上另一个轿厢把客人从换乘层接上并送到目的层所需的时间(要算上开关门的时间),即为模拟双轿厢电梯得到的时间。由于无法对另一个轿厢未来会如何运行进行预测,只能采用这种折中的模拟方式,不够准确,但和真实的结果不会有很大的差距。
本单元中,我设计的调度器主要是适应时间性能指标:通过影子电梯实现局部时间最有。没有仔细考虑耗电量的问题,因为在程序中模拟耗电量以及将耗电量与时间权衡较为复杂。因此在耗电量方面只采用了若双轿厢与普通电梯的模拟时间相同则优先选择双轿厢电梯这一策略。
设计了一个SharedFloor类用来解决这一问题,它的本质上就是一个锁:
public class SharedFloor {
private boolean isFull;
private int resetFlag = 0;
public SharedFloor() {
isFull = false;
}
public synchronized void arrive() {
isFull = true;
notifyAll();
}
public synchronized void leave() {
isFull = false;
notifyAll();
}
public synchronized boolean isFull() {
notifyAll();
return isFull;
}
}
双轿厢电梯在进入换乘楼层时会先通过isFull()询问换乘楼层是否被占用,若没被占用则arrive(),若被占用则在换乘楼层wait(),等待换乘楼层内的另一个轿厢leave()后将其唤醒,被唤醒后则arrive()进入换乘楼层。
最惨的一集,本次作业由于课下评测由于评测不够充分而产生了可以稳定复现的wrong answer。
此bug可能会出现在双轿厢重置请求与普通请求在同一时间戳输入之时:由于在双轿厢模拟的过程中不是每一个轿厢都能够接到某一个请求,所以需要判断某个请求的起始楼层是否在该轿厢的运动范围中。当双轿厢重置请求与普通请求同时输入时,可能会出现一个现象:普通请求在该电梯双轿厢重置前完成了模拟,但需要等双轿厢重置后才RECEIVE,这时候RECEIVE会输出电梯原来的ID而不是重置后的ID。这个bug的根本原因是模拟克隆下的电梯状态并不是实时的,而是滞后的。之后我选择直接输出电梯的ID而非影子电梯的ID便可解决这一问题。
多线程的行为复杂且不可调试,在debug的过程中只能使用print的方法。可以通过打印含有时间戳以及电梯ID的变量来追踪不同电梯线程的状态,虽然麻烦但是很管用。
首次接触多线程编程,通过三次作业的摸索对多线程编程有了一定的了解。
bug出现的频率。笔者由于代码结构层次设计不当导致某些类中出现代码堆积的现象,耦合度大大升高。这同时导致了外部线程与这个线程之间的频繁通信,增大了线程通信的风险。bug真的很难溯源,笔者在第六和第七次作业中都遇到了轮询的问题,通过大量的print才找到问题的根源,既费时又费力。reset过程中涉及了大量的线程通信,所以笔者将其设为原子操作。若将reset操作拆开,则在此架构下会对正确性造成较大的考验。因此选择了放弃部分性能来平衡正确性。