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






简述:第一次作业的要求是标准输入中给定一个带有指定电梯的请求,通过合理的多线程分配,实现目标电梯接受目标请求并且完成。
实现过程:
本次作业(包括之后的作业)使用的是三个主要的线程:
这一次作业是后面作业的基础,因为在输入请求的时候已经规定使用的电梯,因此相当于调度器中就少了选择调度电梯的一步,只需要将请求直接放入对应电梯的队列中即可。
本作业中我采用的电梯运行策略是往届大多数学长学姐们采用的LOOK算法,其基本介绍如下:
具体来说,是这样的:
if (open(floor,num,direction,passengers,elevatorWaitQueue)) {
return Advice.Type.OPEN;
}
if (num != 0) {
return Advice.Type.MOVE; //电梯里面还有人
} else {
// 电梯里面没有人
// 接下来判断请求队列里面是不是空的
if (elevatorWaitQueue.waitQueueEmpty()) {
if (elevatorWaitQueue.isEnd()) {
return Advice.Type.END;
} else {
return Advice.Type.WAIT;
}
} else {
if (!elevatorWaitQueue.OriginDirectionEmpty(floor,direction)) {
return Advice.Type.MOVE;
} else {
return Advice.Type.REVERSE;
}
}
}
如果用一句话概括LOOK策略(也是我当时对LOOK的理解),就是能走就继续走,不到一定要掉头的时候都不掉头。
同时,上述的策略我是作为一个静态方法类储存起来,在电梯线程每次执行run方法的时候,首先调用此方法进行策略的判断。
这个方法是贯穿我的三次作业的电梯的根本调度方法,因为这个方法是能接就接,本身这个方法的目标就是以电梯的消耗量最少为目的的(对立面是以乘客的需求为目的),所以理论上这个算法的耗电量和运行时间的总和会处于一个相对稳定并且低的状态,而且算法理解起来很简单,因此我也推荐LOOK算法。
同步块和锁主要是为了规避多线程运行过程中对于同一个实例对象同时进行读写操作可能引发的冲突问题所设置的一个解决方案。在本次作业中,就如前文所说的,只有RequestTable和ElevatorWaitQueue这两个对象时作为连接线程之间的共享对象,因此在这一次作业中我的选择就是把这两个类中的访问方法全都加上了锁,保证每个时候只有一个线程能对其进行操作。
附带说一句,我对同步块和锁的理解,就是把它们控制的区域块进行原子化,在这其中的执行过程不能有其他的线程进行干扰。
这次的作业比较简单,因此我把那些讨厌的红色的东西运行错误给解决后,交一次就过了(),所以其实我自己并没有找到很多bug。这一次作业主要是让我们了解多线程的构建方法与运行方式,了解多线程的构造,所以相对比较简单但是刚开始真的把我折磨的死去活来的这次的hack时候主要是一些临界测试进行边缘检测,比如刚好在关门瞬间读取请求、电梯满员之后的处理等等。
第二次作业我个人认为是整个电梯调度系统中相对来说比较难的一次作业,相对于第一次作业的变化主要有三点:
因此,这次作业我们需要实现的要点和难点如下:
理论上来说最优的应该是自由竞争,但是由于本次作业的限制,无法实现
其次的应该是影子电梯,其本质就是将电梯的所有状态完全拷贝一份出来,一模一样的模拟电梯跑6遍(每次不同点只在于新加的请求分配电梯的不同),对于其中的sleep()改为Total+=Time。但是因为那个时候事情有点多并且有点懒本人能力有限,怕为了性能分而丢掉了正确性,所以我就没采取这种方法。
其余的方法一般就是三种:
对于Elevator类中的reset实现,我在电梯类中新增了两个私有属性:isReset和ResetRequest,前者是用来代表这个电梯是否正在处于reset的状态,后者是用来储存要被reset的状态。然后对于电梯的reset实现,要分为两步:
public void reset(...) {
openAndCloseForReset();
TimableOutput.println("RESET_BEGIN-" + this.elevatorId);
setMax(max);
setMoveTime(moveTime);
try {
sleep(resetTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
TimableOutput.println("RESET_END-" + this.elevatorId);
setReset(false);
} //电梯重置
接下来就是一个很容易错的点:reset要怎么通知电梯呢?
一般来说就只有两种想法,一种是在输入线程直接通知电梯reset,另一种是在调度器中通知电梯reset。理论上来说这两种方法都行,要看具体实践;但是如果使用的是一个request作为接口的话,我觉得还是直接在输入线程中通知电梯,因为这里的关键是 当输入线程读取到reset后会直接输出reset-accept,在这之后这个电梯就不能receive,如果放在调度器里就必须使得reset一有就立刻提取出来的问题。也就是说reset的优先级要比其他的请求高,这个是必须意识到的。
因此,为了方便解决,我个人是直接把reset通知电梯放在输入线程中:
if (request instanceof PersonRequest) {
MyPersonRequest Myrequest = new MyPersonRequest(...);
waitQueue.addRequest(Myrequest);
} else if (request instanceof ResetRequest) {
MyResetRequest Myrequest = new MyResetRequest(...);
elevators.get(Myrequest.getElevatorId()).setResetRequest(Myrequest);
elevators.get(Myrequest.getElevatorId()).setReset(true);
}
这次作业继承了上一次作业的锁,并且由于电梯的isReset状态需要被调度器所知道,因此我就在电梯的这个reset相关部分都加入了锁。其余的与第一次作业大体相同,没有太大的改动。
因为一些不可抗原因,我错过了这个作业的提交,因此hack的时候没有参加,但是我在自己debug的时候,找到了一些容易错的点(处理上文说的reset和receive):
while(true){
...
if (requestQueue.isEmpty() && requestQueue.isEnd() && TotalReset() /*为了防止reset打回*/) {
for (int i = 0; i < elevatorQueues.size();i++) {
elevatorQueues.get(i).setEnd(true);
}
return;
}
getAndRemove();
...
}
我的wait被封装在这个函数之中,但是我的函数的进入wait的条件是请求总盘子没了但是输入还没结束。这样子的话就会出现轮询,我们考虑一下这种情况,等到输入线程结束后,最后一个请求还在电梯中,这个时候不满足wait的条件,就会导致直接跳过这个wait的地方,而导致这个while(true)循环不断进行,不断消耗CPU的资源。
PS:检查轮询的简单方法:在每个while(true)中加入一个print,如果一直输出就是轮询。
简述:第三次作业主要就是加入了双轿厢电梯的处理,但是其实双轿厢电梯除了换成楼层的问题外,跟普通的电梯其实没有区别,因此我们可以沿用之前的思路,仅仅对分轿厢的时候进行着重考虑。
因为普通电梯和双轿厢电梯的一个其实没有很大的区别,因此我们可以不用新建一个类,而仅仅在原来的电梯类上新增一个标识符,用来表示是原来的电梯还是双轿厢电梯。其余的做出对应的简单改变即可。
对于Split的对电梯的通知,我跟reset一样放在了输入线程中。
需要注意的是,由于Split方法的存在,我们需要重写电梯的出人(out)方法,再换乘层的时候将所有的乘客打下去:
public void out() {
if (Objects.equals(dc, "0") || this.curFloor != transFloor) { //如果是普通的电梯 或者是双轿厢但是不是位于交换层
if (curPassengers.containsKey(curFloor)) {
for (MyPersonRequest request : curPassengers.get(curFloor)) {
outprint();
this.num--;
}
curPassengers.remove(curFloor);
}
} else if (Objects.equals(dc, "A")) { //下层双轿厢且位于交换层
synchronized (totalWaitQueue) {
for (int i = transFloor; i <= 11; i++) {
if (curPassengers.containsKey(i)) {
for (MyPersonRequest request : curPassengers.get(i)) {
outprint();
if (i != transFloor) {
MyPersonRequest r = new MyPersonRequest(request.getPassengerID(),transFloor,i);
totalWaitQueue.addRequest(r);
}
this.num--;
}
curPassengers.remove(i);
}
} //如果是要继续往上走的,就给把他们扔下来,放入总请求队列中
}
} else if (Objects.equals(dc, "B")) {
synchronized (totalWaitQueue) {
for (int i = transFloor; i >= 1; i--) {
if (curPassengers.containsKey(i)) {
for (MyPersonRequest request : curPassengers.get(i)) {
outprint();
if (i != transFloor) {
MyPersonRequest r = new MyPersonRequest(request.getPassengerID(),transFloor,i);
totalWaitQueue.addRequest(r);
}
this.num--;
}
curPassengers.remove(i);
}
}
}
}
totalWaitQueue.wake();
} //电梯让别人出去
然后还有一个就是如何分裂,我选择的是将原来的电梯直接变成双轿厢电梯中的下层电梯,然后再split方法中为电梯数组增加一个新的电梯线程。
由于双轿厢电梯不能同时处于某一楼层中,我们需要同一个id的两个双轿厢电梯共享一个数据,作为双轿厢电梯的换成楼层是否被占用的标识。因此我先建了一个类,来表示这个标识。
public class Occupy {
enum State { OCCUPIED, UNOCCUPIED }
private State state;
public Occupy() {
this.state = State.UNOCCUPIED;
}
public synchronized void setOccupy() {
waitForOccupy();
this.state = State.OCCUPIED;
notifyAll();
}
public synchronized void setUnOccupy() {
this.state = State.UNOCCUPIED;
notifyAll();
}
private synchronized void waitForOccupy() {
notifyAll();
while (state == State.OCCUPIED) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
之后,再上一节说到的增加电梯线程的同时,我们需要将这个occupy类的实例对象加入到另一个电梯中,作为两个电梯的共享对象。
当一个电梯需要到换成楼层时,检查occupy标识,如果时false就正常移动;如果时true则进入waitForOccupy() 函数中进行等待。
这样一来,我们就完成了双轿厢电梯的主要职责。
这次在第二次的基础上增加了部分地方的同步块,主要原因是电梯的调度器的退出条件我选择的是:
boolean flag = true;
synchronized (elevators) {
for (Identity i : elevators.keySet()) {
if (!elevators.get(i).elevatorEmpty()) {
flag = false;
break;
}
}
}
if (requestQueue.isEmpty() && requestQueue.isEnd() && TotalReset() && flag) {
for (Identity i : elevatorQueues.keySet()) {
elevatorQueues.get(i).setEnd(true);
}
return;
}
也就是,我们需要访问电梯的内部状态来判断,因此,在电梯内部对于reset和split对总请求队列进行修改时,要在外面增加一个总请求队列的锁,如:
synchronized (totalWaitQueue) {
if (passengerEmpty()) {
clearPassenger();
} else {
openAndCloseForReset();
}
TimableOutput.println("RESET_BEGIN-" + this.elevatorId);
}
//openAndCloseForReset()中调用了totalWaitQueue
同时,电梯的容器也要被外部访问。因此在涉及对电梯的增加过程中,也要对电梯加锁:
synchronized (elevators) {
changeThis();
ElevatorWaitQueue parallelQueue = new ElevatorWaitQueue();
synchronized (elevatorWaitQueues) {
changeThisQueue();
}
startNew();
}
//简易描述
这次我测出自己的bug和hack别人用的大多数是进程无法正确退出的例子。
在三次作业中,我并没有经历大面积的重构,基本上都是稳步迭代上升
这次的电梯感觉起来比第一单元的难度提升了一个档次,主要因为第一单元是单线程问题,而这一单元的多线程问题就必须考虑同步与互斥的问题,并且在debug的时候会变得相对比较困难(代码的bug不一定能复现)。我自己没有啥特殊的debug的技巧,我一般都是对着那次出错的数据结论,根据电梯的id进行筛选,观察一个电梯经历的变化过程。虽然好像确实不是很有用,但是这样确实帮我找到了挺多的bug()。
下面从线程安全和层次化两个角度说一下我的感受:
首先是线程安全。线程不安全其实是导致电梯主要的不容易发现的bug的所在,这个主要是因为同步块和锁的不恰当的加入(一般是少加了),导致在一个地方执行对共享对象的修改的一半地方,突然被另一个线程中的对这个共享对象的改变所打断,最后造成这个共享对象的值的不确定性。因此,我们需要仔细确定线程之间的共享对象,对其进行同步与互斥处理。
然后是层次化设计。这个其实也是面向对象编程的一个很重要的思想,我们只关注这个类应有的职责,而不去过多的考虑它与其它类的耦合之处。就比如电梯作业,我们在写的时候,输入线程、调度器线程、电梯线程都是相互独立的,我们只需考虑他们自己的职责进行编写,只不过在其中增加一些共享对象进行连接。
这次电梯作业说实话还是很有挑战性的,主要是多线程的不确定性让这个电梯更有了神秘色彩。