301
社区成员
发帖
与我相关
我的任务
分享本篇总结很长,有很多平淡无趣的部分,推荐阅读的部分为
- 为什么电梯要继承
- 销毁大于转化
- 分配策略——超越影子电梯

架构图如上,大概分为以下几个部分
Thread,实现了run方法Contoller接口,ControllerManager管理所有的Controller本次的架构我认为设计的还是非常优秀的,架构设计的理念主要是
在本次作业中,加入了多线程的概念,在设计的开始,我们就应该思考,应该将哪个部分作为线程,在这个问题下,一般有以下三种实践:
其中,第一种实践是最差的,会导致电梯的代码包含了控制逻辑,并且在第七次作业的迭代中导致电梯难以被继承(大量冗杂的代码都需要重写)。
简单的来说,第一种方式的错误在于电梯不是线程
电梯就是电梯,除了上下开关不应该有其他功能
为了搞清楚是否需要单独实现线程类,我们首先要想清楚,线程中需要做什么
简单来说,线程做了以下三个任务:
如下代码所示,线程的run方法实际上是公式化的
// run from ControlThread
public void run() {
while (true) {
// check wait condition
if (autoController.noRequest() && elevator.isEmpty()
&& !autoController.inReset() && !autoController.ended()) {
synchronized (autoController) {
try {
autoController.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// check exit condition
if (autoController.ended()) {
try {
elevator.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
// control elevator
autoController.elevatorControl();
}
}
以上是普通电梯运行的线程代码,循环中代码基本分为三个部分
autoContoller获取电梯运行状态,检查是否需要等待实际上,2、3两种实现都是可用的,我选择了单独实现线程类略微降低了代码的耦合,但是会导致多出来多个单独的线程类,本着线程类的作用和Main的作用同等的设计理念,建议采用方法3
这里主要涉及第七次作业的迭代,在我的架构中,基本上存在两个部分的继承
Contoller接口,保证不同控制类在ContollerManager眼中是均等的其实不不继承问题也不大(因为电梯中没有运行逻辑),这里涉及一个小的trick
相信大多数同学在第七次作业中,都重新判断了电梯类型,然后分开输出普通电梯的输出和双轿厢电梯AB电梯的输出,然而有一个方便的方法可以简化代码
在普通电梯类中实现toString,输出电梯号,调用toString输出
// just print arrive at current level
public synchronized void arrive() {
TimableOutput.println(
"ARRIVE-" + curLevel + "-" + toString()
);
}
@Override
public String toString() {
return Integer.toString(id);
}
双轿厢电梯类中重写toString,输出电梯号-电梯类型
@Override
public String toString() {
return super.getId() + "-" + label;
}
奇妙的事情发生了,当我们调用双轿厢电梯中继承的方法时,输出的信息会携带label!
因为双轿厢电梯不是一个电梯,而是两个电梯
所以继承的控制器实际上是每个电梯的控制器,我还实现了一个总控制器,它运行在单独的线程中,调度AB电梯的请求和控制中间层的交互
而在ContollerManager(负责分配请求和结束线程)眼中看来,双轿厢总控和普通控制器是一样的
public interface Controller {
boolean inReset();
void resetElevator(int capacity, double moveTime, boolean dc, int level);
void exit();
boolean noRequest();
void addRequest(Passenger passenger);
boolean overload();
double requestScore(Passenger passenger);
boolean elevatorEmpty();
}
终于到了这个作业中最麻烦的地方,为了设计线程之间的交互,首先看看在我的设计中,线程间的关系如何

ContollerManager从输入线程接受请求,并且分配给其他ContollerDcControlThread负责接受请求,再分配给DcSubControlThread中运行的DcAutoContoller,DcContoller负责和ContollerManager通信,DcAutoContoller负责控制电梯的行为其实就两个点
本次作业的所有更换行为(reset送回请求和重置双轿厢电梯),都推荐直接销毁旧的乘客、电梯,换成新的
此处需要修改请求的From,容易忘,直接销毁乘客创建新的就好
if (dcReset) {
exit();
DcController dcController
= new DcController(id, controllerManager, resetSwitchLevel);
DcElevator upperElevator
= new DcElevator(id, Elevator.getMaxLevel(), resetSwitchLevel, "B");
upperElevator.set(resetCapacity, resetMoveTime);
DcElevator lowerElevator = new DcElevator(id, resetSwitchLevel, 1, "A");
lowerElevator.set(resetCapacity, resetMoveTime);
dcController.setUpperElevator(upperElevator);
dcController.setLowerElevator(lowerElevator);
DcContolThread dcContolThread
= new DcContolThread(dcController, controllerManager);
dcContolThread.start();
controllerManager.removeController(id);
controllerManager.addController(id, dcController);
}
ContollerManager,并移除自身挺好解决的,每个轿厢试图进入交换楼层时会有以下行为
@Override
protected void transport() {
Passenger passengerAtCurLevel;
// fetch passengers in current level
while (true) {
if (getElevator().isFull()) {
break;
}
passengerAtCurLevel = getRequest(getElevator().getLevel());
if (passengerAtCurLevel == null) {
break;
}
try {
getElevator().open();
} catch (InterruptedException e) {
e.printStackTrace();
}
getElevator().addPassenger(passengerAtCurLevel);
}
try {
getElevator().close();
} catch (InterruptedException e) {
e.printStackTrace();
}
setMoveDir();
// set flags
boolean releaseFlag = false;
if (inSwitchFlag) {
inSwitchFlag = false;
releaseFlag = true;
}
move();
if (releaseFlag) {
dcController.releaseSwitch();
}
ArrayList<Passenger> passengers = null;
if (getElevator().hasOutPassenger()) {
try {
getElevator().open();
passengers = getElevator().dropAllPassengers();
getElevator().close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (inSwitchFlag) {
requirement = false;
if (passengers != null) {
sendRequestToSwitch(passengers);
}
}
}
叠个甲,hw6强测99.79,hw7强测99.93
首先,你的轿厢应该LOOK
对于双轿厢,我采用的LOOK算法会永远把上/下界限定为换乘层(这不一定有益于性能)
实际上好的调度策略只需要满足两点
从这两个理念出发,我们来看看现有的几个分配策略
研讨中提到得分分配的同学均认为得分分配性能不如影子电梯,但是实际上我所采用的得分分配比同组采用影子电梯的同学性能略好,恐怕是建模方式不同导致的
要想超越影子电梯,首先我们来想一想,影子电梯的缺陷在哪里
自由竞争是动态分配,防止了电梯的木桶效应
这是本学期的分配要求下无法做到的,本学期的分配由于RECEIVE约束,只能是静态分配,因此一定会产生木桶效应,即由于多线程程序的随机性和数据的随机性,最终容易产生少数电梯仍然存在请求,多数电梯请求已经完成的情况
一梯有难,五梯围观
影子电梯产生的是局部最优解,无法预知未来
先上实现
ControllerManager中的分配方法
分数最低的为分配电梯(这个意义上,这可能更应该叫做预估时间或者惩罚值)
// acquire controller id by distribute algorithm
// return -1 if there is no available elevator
private int distributeElevator(Passenger passenger) {
int controllerId = -1;
double score = 1e6;
for (int key : autoControllers.keySet()) {
Controller autoController = autoControllers.get(key);
// if elevator is in reset or elevator is overload
// (all request received > 1.5 * elevator capacity)
// pass
if (autoController.inReset() || autoController.overload()) {
continue;
}
double controllerScore = autoController.requestScore(passenger);
if (controllerScore < score) {
score = controllerScore;
controllerId = key;
}
}
return controllerId;
}
普通电梯
public synchronized double requestScore(Passenger passenger) {
double t1 = requestFromPassengerTime(passenger);
double t2 = requestTargetPassengerTime(passenger);
double c = elevator.getPassengerCount();
double m = elevator.getCapacity();
double r = requestSize();
double overload = c + r - m > 0 ? c + r - m : 0;
return t1 + 0.5 * (t2 - t1) + 0.4 * (c + r) + 1.5 * overload;
}
分数的构成分为三个部分
t1 + 0.5 * (t2 - t1)0.4 * (c + r)1.5 * overload其中,magic number均为参数,可以调整,这是简单实验了几组最终选择的参数
双轿厢电梯
双轿厢电梯由于电量优势和吞吐量优势,请求会更可能被分配给双轿厢电梯
// final score from DcController
@Override
public synchronized double requestScore(Passenger passenger) {
boolean onlyLower
= passenger.getFromLevel() <= switchLevel
&& passenger.getTargetLevel() <= switchLevel;
double upperScore = upperController.requestScore(passenger);
boolean onlyUpper
= passenger.getFromLevel() >= switchLevel
&& passenger.getTargetLevel() >= switchLevel;
double lowerScore = lowerController.requestScore(passenger);
if (onlyUpper) {
return upperScore;
}
if (onlyLower) {
return lowerScore;
}
return upperScore + lowerScore;
}
// score of each elevator from DcAutoController
@Override
public synchronized double requestScore(Passenger passenger) {
// doesn't switch
if (label == Label.Up) {
if (passenger.getFromLevel() >= getElevator().getMidLevel()
&& passenger.getTargetLevel() >= getElevator().getMidLevel()) {
return super.requestScore(passenger) * 0.8;
}
} else {
if (passenger.getFromLevel() <= getElevator().getMidLevel()
&& passenger.getTargetLevel() <= getElevator().getMidLevel()) {
return super.requestScore(passenger) * 0.8;
}
}
double c = getElevator().getPassengerCount();
double m = getElevator().getCapacity();
double r = requestSize();
double overload = c + r - m > 0 ? c + r - m : 0;
double requestTime;
// require switch
if (label == Label.Up) {
if (passenger.getFromLevel() > getElevator().getMidLevel()) {
requestTime = requestTime(passenger.getFromLevel(),
getElevator().getMidLevel(),
getMoveDir(), upperLimit(),
lowerLimit(), getElevator().getMoveTime());
} else {
requestTime = requestTime(getElevator().getMidLevel(),
passenger.getTargetLevel(),
getMoveDir(), upperLimit(),
lowerLimit(), getElevator().getMoveTime());
}
} else {
if (passenger.getFromLevel() < getElevator().getMidLevel()) {
requestTime = requestTime(passenger.getFromLevel(),
getElevator().getMidLevel(),
getMoveDir(), upperLimit(),
lowerLimit(), getElevator().getMoveTime());
} else {
requestTime = requestTime(getElevator().getMidLevel(),
passenger.getTargetLevel(),
getMoveDir(), upperLimit(),
lowerLimit(), getElevator().getMoveTime());
}
}
return 0.8 * requestTime + 0.4 * (c + r) + overload;
}
双轿厢电梯的分数由总控制器提供给ControllerManager,分数的构成由每个电梯的分控制器提供
分控制器的分数为
这个部分当时比较懒,没有认真设计,不过最后结果证明效果不错
可以看到,关键的地方在于,不仅要预估到达时间,还要考虑载重和已有请求产生的罚时
最后的模型如下
$$ score = \alpha \cdot directTime + \beta \cdot requestPanalty + \gamma \cdot overloadPanalty $$
说实话,没那么大必要,随便调一下就行,对性能的影响没那么大
如果真的要最优解,可以考虑启发式算法+随机数据模拟跑个一万组
还有两种分配策略恐怕可以达到最优性能
强化学习是说笑的,带权随机真的可以尝试一下
本次作业最容易出bug的地方就是各种各样的条件判断中漏项导致的bug
如果作业的实现合理,不滥用同步块,那么死锁问题应该并不容易遇到(我只遇到过一次,是由于滥用同步块导致的,直接重构)
然而条件判断......每次互测都死在这个bug上。
// a part of thread run()
// 4 different conditions is needed here
if (autoController.noRequest() && elevator.isEmpty()
&& !autoController.inReset() && !autoController.ended()) {
synchronized (autoController) {
try {
autoController.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// a method in ContollerManager
// method for condition check
public synchronized boolean ended() {
if (exit) {
return true;
}
if (inputFinish && allControllersNoRequest()
&& allElevatorsEmpty() && noRequest()
&& !anyElevatorInReset()) {
return true;
}
return false;
}
如上所示,很多地方需要4个甚至5个条件判断,有的部分甚至需要把条件做二次抽象
比上一次作业写得爽,果然合理的架构是关键
从单线程到多线程的过程并不是一番通顺,确实hw5-7相较于之前有难度上的跃升。不过只要架构合理,迭代就不会特别痛苦
期待下一次作业