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

Main类:用于创建并开启Input线程,Schedule线程和6个Elevator线程。RequestQueue类:定义了ArrayList<PersonRequest> requests变量存储请求列表,同时定义了共享变量的读写操作,用于实现多线程之间的同步与互斥。InputThread类:同于读取请求,与Schedule线程共享一个RequestQueue变量,将读取的请求加入其中。Schedule类:同于将读取的请求分配给电梯,分别与六个电梯线程共享一个RequestQueue变量。本次作业中由于请求指定了电梯,故直接按请求分配即可。Elevator类:为本次作业核心部分,承担了将乘客运送到目的地的任务。定义OVER, WAIT, MOVE, REVERSE, OPEN这五种策略并定义strategy()函数根据电梯当前状态返回下一步策略。电梯线程只需要通过strategy()获取策略并执行相应的动作。if (canOpenForIn() || canOpenForOut()) {
return Strategy.OPEN;
} else if (getPasNum() != 0) {
return Strategy.MOVE;
} else {
if (waitQueue.isEmpty()) {
if (waitQueue.isEnd()) {
return Strategy.OVER;
} else {
return Strategy.WAIT;
}
} else {
if (notReverse()) {
return Strategy.MOVE;
} else {
return Strategy.REVERSE;
}
}
}
采用look算法,先判断是否开门,若电梯内有目的地为该层的请求,或该层的等待队列中有同向的请求且电梯内可以接受请求,则返回``OPEN开门。若不能开门则判断电梯内部是否有请求,若有则返回MOVE继续移动,否则判断等待队列若为空且且为end(表示调度结束)则返回OVER结束线程,若不为end返回WAIT表示等待。若等待队列不为空,则表示还能继续运行,判断电梯前方的楼层是否有等待的请求,若有则继续移动,否则表示请求在相反方向,返回REVERSE调转方向。
RequestQueue类中所有对共享变量的操作上加锁,加锁对象为RequestQueue对象本身,在getAndRemove()函数获取请求时若为空则等待,在add()与setEnd()函数中会进行唤醒。OPEN策略时使用wait()来实现,当在等待电梯关门的过程中如果能接收新的请求的话则唤醒电梯接收请求。因此,Elevator中执行OPEN的函数与Schedule类中分配电梯的部分设为同步块,使用电梯对象作为锁。OPEN时使用wait()实现,在电梯移动的过程中如果有位于电梯出发层的请求则电梯重新开门接收请求。因此,Elevator中执行MOVE的函数与Schedule类中分配电梯的部分设为同步块,同样使用电梯对象作为锁。

本次作业修改或新增的类有:
RequestQueue类:改用HashMap<Integer, ArrayList<PersonRequest>> requests,按照起始楼层存储对应的请求列表,这样方便电梯线程接受请求的操作。Elevator类:改用HashMap<Integer, ArrayList<PersonRequest>> taskQueue按照抵达楼层存储对应的请求列表,这样在后续实现电梯进出乘客的功能时可以更快速的找到相应的请求。由于在重置中的电梯不能接受请求,因此定义ArrayList<PersonRequest> waitForReceive变量,当电梯重置时先将分配的变量加到此Input线程完成读取后不能立刻将Schedule的请求队列setEnd(),考虑若Schedule中请求队列为空,而还有电梯正在重置,需要将电梯的请求再返回到调度器中重新调度,若此时Schedule线程已经结束则无法进行重新调度。我的解决方法是在Input线程读取完后再等待2s, 这样即使最后一个请求是reset请求也会等到电梯重新分配完再结束Schedule线程。在分配请求时,我选择根据电梯到达当前请求所需时间、电梯等待队列和电梯内部人数、容量以及是否重置这四个点计算每个电梯的cost值,选择代价最小的电梯进行分配。在计算电梯的cost值时,首先考虑电梯到达当前请求的时间,若电梯为运动状态且方向与当前请求与电梯的方向相反,则考虑电梯到达最远端楼层并折返回来的时间。若电梯运行方向相同则计算电梯直接到达该请求的时间。需要注意,在电梯为等待状态和重置状态且没有分配请求时方向是可以改变的,因此此时计算到达时间时都按照电梯直接到达该请求来计算,并将电梯方向设为此方向。而在后面的请求到来时则需要考虑电梯的方向进行计算。还需要考虑电梯等待请求的数量与电梯容量,若总请求数小于电梯容量则cost值不变,否则乘上一定的权重加到cost。这样做,在请求数量少时可以只使用少数电梯完成请求,减少耗电量,而在请求数量多时也不会出现大量请求分配给少数电梯的情况,更加合理。同时考虑到电梯完成重置需要一定的时间,因此给重置状态电梯的cost加上一定的偏移。
这种调度方案虽然计算没有影子电梯准确,但是实现起来比较简单,而且最后的强测中性能分也都比较高。
除了RequestQueue类中的同步块与hw5中相同外,还新增了以下同步块:
Input类中设置电梯为重置状态的函数与Schedule类中计算所有电梯cost值并选择调度电梯的函数设为同步块,这两个同步块的锁是同一个对象。防止在计算电梯cost时前面计算过的电梯发生重置,导致调度选择的结果出错。Schedule类中调度函数计算发现所有电梯都处于重置状态时加锁等待,在电梯重置结束时加锁唤醒,这两个同步块的加锁对象相同。

架构与hw6基本一致。
本次作业新增了双轿厢电梯重置。由于在双轿厢重置时需要增加新的电梯,因此我将在Main类中定义的电梯列表改为Elevator类的一个静态变量,这样方便电梯进行重置时向其中增加新的电梯。在电梯类新增Elevator类变量存储双轿厢电梯中另一个电梯,并且者两个电梯不共用同一个等待队列。
在实现双轿厢中两个轿厢不碰撞中,为每个电梯设置一个flag变量判断该电梯是否正在前往换成楼层或正处于换成楼层。当其中一个电梯执行MOVE策略且目的地为换成楼层时,判断另一个电梯如果flag有效,则阻塞等待,否则将flag设为有效并继续移动。当电梯离开换成楼层时将flag设为无效并唤醒另一个正在等待的电梯。当电梯执行WAIT且处于换成楼层时,让电梯移动一层,这样防止另一个电梯要进入换乘楼层时等待时间过长。
此外,本次作业线程的结束条件也需要改变。由于电梯不共用等待队列,当双轿厢电梯中一个电梯还在运行,另一个电梯没有请求时结束线程,这样当还在运行的电梯将请求送到换乘层时将没有电梯来继续完成请求。因此双轿厢电梯结束的条件为,当前电梯没有请求,且同一轿厢中另一个电梯处于等待状态或已经结束。这说明两个电梯都没有请求需要处理,且当等待队列的end有效时即可结束线程。在结束线程时,需要将另一个等待中的电梯唤醒,使其也结束线程。
本次作业除了与hw6中相同的同步块外,还新增了以下同步块:
Schedule类中遍历电梯列表的部分与Elevator中实现双轿厢重置新增电梯的部分都对共享对象elevators进行读写操作,需要加锁使其变成同步块。Input线程读取请求,Schedule线程分配请求,Elevator线程完成请求。Schedule调度的实现:都是根据不同电梯对当前请求cost值的比较来选择分配电梯cost值的方法:hw5是直接根据请求给定的电梯,hw6中需要考虑电梯的当前状态,hw7中还需要考虑新增的双轿厢电梯。Schedule线程。hw7中双轿厢电梯的结束,一个电梯不能在没有乘客时立即结束,需要等待两个电梯都没有乘客时才能结束。WAIT状态且处于换乘层时,需要设置电梯方向并使其移动一层。而在计算cost的函数中当电梯为等待状态且无请求时会改变其方向。由于这两处没有设置为同步块导致电梯可能朝相反方向移动,从而超过换乘楼层导致出错。wait()函数前后print信息,然后根据运行时打印的信息来锁定程序是卡在了哪些同步块中。通过完成这一单元作业,我认为在多线程程序的编写过程中要保证线程安全,最重要的是保证对共享数据访问的互斥。需要找出程序中所有的共享变量并设置同步块保证线程互斥。正如本次作业中线程的共享对象不只有RequestQueue这一个变量,还有elevator对象为Input线程与Schedule线程的共享对象,elevators存储电梯列表的变量为Elevator线程与Schedule线程的共享对象,以及Elevator类中的某些属性也为共享变量,在程序编写中需要全面地思考并找到这些共享对象才能保证整个程序的线程安全。
Input线程读取请求并交给Schedule线程Schedule线程分配请求给电梯strategy()函数根据当前电梯状态给出电梯下一步的策略