301
社区成员
发帖
与我相关
我的任务
分享
Mainclass: 代码入口, 负责实例化各个托盘对象
Input: 输入类, 线程类, 实时读入电梯请求并放入总请求队列
Schedule: 调度类, 线程类, 根据读入的线程进行请求分配
Elevator: 电梯类, 线程类, 实时运行一个电梯
Strategy: 策略类, 根据电梯当前状态提供电梯运行策略
ShadowElevator: 影子电梯类, 负责模拟电梯的运行时间

整体任务分析: Unit2我们的作业围绕电梯调度问题展开, 我们需要实现一个电梯服务系统来满足不同的服务请求.
整体架构设计: 在本单元中, 因为本单元的核心是多线程项目的开发, 因此最常见的设计架构便是生产者-消费者模式. 在我的代码设计中, 我使用了两类托盘来满足这一需求. 第一类托盘为Input-Schedule托盘, 即Input类生产请求, 同时由Schedule类消费. 第二类托盘为Schedule-Elevator托盘, 即Schedule生产请求, Elevator类消费请求. 第一类托盘由一个, 第二类托盘对每一部电梯都设置一个托盘.
整体同步设置: 同步块的目的是防止出现数据冲突, 因此需要对共享对象的读写操作增加synchronized关键字来保证同步互斥, 在我的架构中, 同步块主要被设置在两类托盘的存取中.
任务: 在这次作业中, 我们需要实现6部电梯, 让它们分别服务请求. 本次作业提供的请求会指定电梯.
难点: 由于每个请求都被指定了电梯, 因此我们的调度只需要根据指定的电梯id进行请求分发即可. 主要需要实现的是电梯的运行策略和捎带策略.
实现: 在我的项目中, 我参考了大多数学长的方法采取Look策略进行调度. 实现方式如下:
if (!NoInOut()) {
InAndOut();
} else {
if (!ElvEmpty()) {
MoveDir;
} else {
if (NoRequest()) {
Wait();
} else {
MoveToNear();
}
}
}
任务: 第六次作业中, 输入的请求不再要求指定特定电梯, 而是由我们的程序自行判断; 同时这次作业还新增了receive和reset服务.
难点: 本次作业可以被解构为两种功能迭代. 第一是实现对电梯reset请求的支持, 第二是实现电梯对请求的调度.
实现: 由于电梯调度本质上只是提供一个数字, 即电梯id, 实现复杂度较低, 因此我优先设计了对reset请求的满足. reset请求要求电梯尽快停靠并且释放电梯内的所有人, 同时对电梯自身的参数进行调整, 原本调度给当前电梯的请求无效化. 因此我将电梯的服务优先级改为如下方式:
while (true) {
if (!resetList.isEmpty) {
elvReset();
} else {
elvAction();
}
}
即保证电梯在每一个动作之后都判断是否进行reset, 从而保证了reset的尽早满足. 在reset时, 由于需要将原本调度过的请求无效化, 我采取将电梯的请求队列重新放回总队列进行重新调度来满足. 而针对有可能将一个请求分配给一个正在reset的电梯的情况, 由于电梯在reset过程中不能输出receive信息, 我设计了缓冲队列方式.
//Schedule类
Elevator elevator = elevators.get(elvId);
if (elevator.isReset) {
bufferList.add(request);
} else {
requestList.add(request);
}
//Elevator类
public void elvReset() {
//code to support reset
if (!bufferList.isEmpty) {
requestList.addAll(bufferList);
printReceive(bufferList);
bufferList.removeALl();
}
}
在实现了reset之后, 我开始设计电梯的调度模式. 在我的设计中, 我采取的是模拟影子电梯的调度策略. 通过设计ShadowElevator类, 并且对每一部电梯都实例化一个影子电梯对象, 在每次进行调度时, 更新各个电梯的状态, 通过模拟电梯运行并计算最短的结束时间, 最短时间的id就是分配的电梯id.
public int allocByShadow(MyRequest myRequest) {
ArrayList<Integer> time = new ArrayList<>();
for (int i = 1; i <= 6; i++) {
shadowUpdate(i, myRequest);
time.add(shadowElvs.get(i - 1).evaluateTime());
}
int elvId = 1;
for (int i = 2; i <= 6; i++) {
if (time.get(i - 1) < time.get(elvId - 1)) {
elvId = i;
}
}
return elvId;
}
任务: 在第六次作业的基础上, 本次电梯的reset请求新增了成为双轿厢电梯的请求
难点: 首先, 双轿厢电梯要求一个电梯井内的两个电梯不能发生碰撞, 即两个电梯不能同时处于换乘层, 其次双轿厢电梯的实现方式同样需要合理的设计.
实现: 为了尽可能少的对原有架构进行改动, 我并没有新增类, 而是通过在Elevator类线程运行的过程中启动另一个线程来实现双轿厢. 针对调度, 在分配时针对六部电梯分别查看应当分配给A轿厢还是B轿厢还是普通轿厢, 因此总体改动较小.
public void putRequest(MyRequest myRequest, int flag, int elvId) {
if (flag != 2) {
if (resetFlag) {
elevatorBuffers.get(elvId - 1).addRequest(myRequest);
} else {
elevatorMaps.get(elvId - 1).addRequestFrom(myRequest);
}
} else {
elevatorMapsB.get(elvId - 1).addRequestFrom(myRequest);
}
}
针对防止轿厢相撞, 我首先让电梯在停靠wait时不允许停靠在换乘层
public void elvMoveFree() {
if (Objects.equals(type, "A")) {
elvMoveDown();
} else if (Objects.equals(type, "B")) {
elvMoveUp();
}
}
而对于两个电梯防止同时进入换乘层, 我采取了讨论区的一位同学的设计方法, 通过一个transferFlag托盘来进行两个电梯之间的沟通, 当一个电梯进入换乘层时占有锁, 离开时释放锁.
感谢伟大的舍友和伟大的cxc同学提供的评测机, 让我得以在中测提交通过的基础上更加全面的发现自己的问题. 在这一单元的bug修复中, 主要容易出现的两种bug是轮询和无法结束. 针对轮询, 可以采用print输出调试信息, 在每一个可能出现轮询的循环中加入调试信息的输出从而判断问题的产生; 针对无法结束, 一般情况下极有可能是存在线程正在wait, 却没有其他线程能够唤醒该线程, 从而导致RTLE, 针对这种情况, 考虑在每个线程的wait前后增加输出, 观察是哪一个线程没有结束wait.
这一部分事实上在理论课中老师已经讲的很清晰了, 线程安全会发生的地方就是对于共享数据的同时读写, 因此要做的就是在可能发生同时读写的共享对象的方法上加上同步块即可.
