这三次做业采用了主线程获取请求,多级调度器逐级分派,电梯模拟运行的策略。具体来讲,主线程实例化ElevatorInput
类,经过阻塞读取方式得到请求Request
,以后将请求分配给调度器Scheduler
,调度器负责处理请求(既能够本身处理,也能够分配给其余子调度器处理),每个电梯与一个ElevatorScheduler
绑定,用于具体的调度、睡眠控制、打印输出。java
本次做业的难点主要有如下几点:python
如何控制调度器、电梯线程的终止:简单的生产者-消费者模型中,生产者不断生产,消费者不断消费,不存在线程终止现象;现实中的电梯,一天24小时运行,没有异常状况也不会终止。可是更多的多线程问题是须要考虑线程终止的。这三次做业也是如此:主线程将全部请求都发送给调度器后,告知调度器准备结束,调度器处理完本身队列中剩余请求后,结束线程。git
这种通常性的线程生命周期能够用这个比喻来讲明:假如你是一个员工,在上班的时候你可能在干活,也可能闲着,但即便闲着,也不能回家;到了下班时间后,若是你的任务尚未完成,那就继续工做(加班),若是完成了,就能够回家了。因而,咱们能够抽象出两个因素:是否空闲(idle
),是否到达下班时间(在电梯的问题中是输入是否结束,可用inputEnd
变量表示)。是否空闲是能够本身判断的,而是否到达下班时间则须要外界的通知。如下讨论这种通知输入结束机制。github
一种比较容易想到的方法是采用interrupt
机制算法
while (true) { Request request = elevatorInput.nextRequest(); if (request == null) { scheduler.interrupt(); } else { // pass } }
可是这种方法并不能实现精确控制:咱们但愿的是,若是调度器在等待下一个输入(wait()
函数中),就打断;而若是它在执行别的任务,好比sleep(100)
,或者是其余的同步任务,就不打断。虽然在这一单元的做业中没有出现这种状况,可是多作这种考虑也是合情合理的。编程
另外一种方法是专门设定一个setInputEnd()
方法,由主线程调用,告知调度器输入结束。安全
class Scheduler { boolean inputEnd = false; Queue<Request> requestQueue; public synchronized void setInputEnd() { this.inputEnd = true; notifyAll(); } public synchronized void addRequest() {} public synchronized void getRequest() { while (requestQueue.isEmpty()) { wait(); } return requestQueue.poll(); } }
我在做业中采用的是这种方法,可是后来发现,其实还能够用一种更加简洁的方法解决:创建一个TerminationRequest
类。多线程
interface Request {} class PersonRequest implements Request {} class ElevatorRequest implements Request {} class TerminationRequest implements Request {}
这样,经过归一化处理,能够用一个队列来统一管理(更通常的来讲,是把全部线程关心的状态改变的通知都放到统一的容器中管理,对外用一样的接口,对内采起不一样的处理策略)。同时,这种归一化也方便了线程安全容器的使用。架构
区分两种获取请求的模式:在简单的生产者-消费者模式中,消费者在没有商品的状况下老是会在共享区中的wait
函数中等待,可是在实际生活的不少状况下,这种状况是不可接受的——消费者可能还有其余事情要完成,使用wait
函数等待并释放CPU资源当然是一种进步,但这种方案同时也制约了消费者进行其余活动的自由。回到本单元做业,每个电梯在行为上都须要实现:获取新的请求,决定电梯的行为(开门、关门、上行、下行等)。可是这两种行为并非时时刻刻都须要进行的:若是电梯局部队列为空,电梯内部没有乘客,则电梯处于空闲状态,此时不须要频繁决定电梯的行为,只须要等待下一个请求的到来。所以,当电梯空闲时,应当阻塞地读取请求,即在请求处等待下一个请求;而当电梯忙碌时,则只须要查看并更新请求便可,没有新请求也不阻塞。app
这两种不一样的模式,能够本身解决,如:
class ElevatorScheduler { private Queue<Request> queue; private Queue<Request> localQueue; private synchronized Request getRequest(boolean quickRetreat) { if (quickRetreat) { return queue.poll(); // if the queue is empty, return null } else { while (queue.isEmpty()) { wait(); } return queue.poll(); } } }
也能够采用Java内置的BlockingQueue
来解决:
private BlockingQueue<Request> queue; private void update() { if (idle) { localQueue.add(queue.take()); } queue.drainTo(localQueue); }
灵活的分配器:第一次做业只有一个电梯;第二次做业有多个电梯,但只有一种型号;第三次做业有不一样型号的电梯,每一种电梯型号下的电梯数是不一样的。能够这样认为,第一次做业的电梯只须要一级调度器(直接指挥电梯的调度器),第二次做业的电梯是两级调度(一级负责电梯见的负载均衡,另外一级负责直接指挥电梯),第三次做业的电梯是三级调度(一级总调度器负责换乘相关管理,一级负责同一个类型的电梯的负载均衡,一级负责直接指挥电梯)。以下图:
为了使得分配更加灵活,给这些Scheduler
设计一个统一的接口RequestReceiver
便可,至于内部的处理,或分配或自行指挥电梯,请求提供者都没必要关心。
interface RequestReceiver { void addRequest (Request r); } class CentralScheduler implements RequestReceiver {} class Scheduler implements RequestReceiver {} class ElevatorScheduler implements RequestReceiver {}
反馈和闭环控制:在实际多线程编程中,反馈和闭环控制也是十分常见的。本单元做业也不例外:换乘须要进行请求的反馈,即电梯运行一部分请求后,由另外一个电梯继续完成另外一部分请求。既然电梯是逐级控制的,电梯处理完本身应该处理的那一部分请求后,须要将请求反馈给上级调度器,由上级调度器进行二次分配。另外一方面,调度算法在进行调度时,也须要考虑各电梯的负载均衡问题,于是电梯也要上报自身的负载状况。
这几回做业中,能够经过相应线程类提供反馈接口,进行逐级反馈状态:
interface FeedbackReceiver { void offerFeedback (Feedback fb); void offerRequestFeedback (Collection<PersonRequest> requests); } class CentralScheduler implements RequestReceiver, FeedbackReceiver {} class Scheduler implements RequestReceiver, FeedbackReceiver {}
在反馈反向传播的时候,每一级Scheduler
也能够对反馈进行处理,好比做业3中的负载,每一类电梯的负载能够取这一类全部电梯中负载最小的电梯的负载。
楼层映射:这个问题其实并无什么面向对象的困难,主要是一个小技巧。每一个电梯调度器(直接指挥电梯进行运动的调度器,它实现了调度算法)有一个映射,实现楼层到楼层下标的快速转换。
与其经过数学方法实现(分段函数):
int flr_to_ind (int flr) { if (/* some conditions */) { // do something } else if (/* ... */) { // do something } else { // pass } }
不如用Java自带的方法:
List<Integer> flrs = Arrays.asList(-3, -2, -1, 1, 2, 3); index = flrs.indexOf(flr); flr = flrs.get(index);
假如个人第三次做业真正实现了第一部分中所叙述的思想和方法,那么再进行扩展也不会很复杂了。但事实上个人第三次做业并无彻底实现这些方法和技巧——程序的主题部分是第五次做业时构建的,以后只作了些小修小补。可是毕竟结构是相似的,也能够作一些分析:
Request
接口下增长一个紧急制动请求的实现,调度器将这一请求分派到对应电梯。电梯到达下一个停靠点时,经过反馈渠道反馈全部的未完成请求,由上层调度器二次分配,同时电梯线程结束运行。从SOLID角度看:
ElevatorScheduler
类的职责部分重叠,耦合太高。在一开始的设计中,电梯的职能被规定为负责输出和睡眠(由于这两方面相对固定,能够与易变的ElevatorScheduler
分离,可是在以后的迭代开发中,逐渐职能扩充。RequestReceiver
、FeedBackReceiver
、Runnable
方法,也算是有一点ISP的意思了。main
函数线程只依赖RequestReceiver
接口,也有一点DIP的意思了(虽然没有实现)。考虑到三次做业结构一脉相承,每次迭代又没有什么重大改动,就只分析最后一次做业了。
UML图:
这里只实现了二级分派结构,其中PersonTransfer
是课程组提供的PersonRequest
类的子类,表示须要换乘的乘客请求。二级分派结构能够解决这三次做业的问题,main函数获取请求,再由高级调度器分派给低级调度器,低级调度器与电梯类协做,实现look电梯调度算法。在算法的实现过程当中,须要管理楼层信息、管理用户请求信息,这些管理由building类和floor类处理,同时设置FloorNumberManager
类,提供一些静态方法管理楼层映射、可达性查询等服务。
这种结构的主要问题是没有处理好电梯类Elevator
和电梯调度器类ElevatorScheduler
之间的关系。电梯调度器类只拥有一个电梯,负责这个电梯更细致的调度管理,如每个时间节点,决定电梯上行、下行、开门、关门等动做,主要实现了算法。但同时,电梯类不只负责输出、睡眠,还负责管理电梯内部人员,检查到达目的地的乘客,反馈电梯内部乘客信息等。在具体实现中,电梯类又将自身容器暴露给电梯管理类,使得两个类之间耦合度较高。
此外,电梯类Elevator
并无成为一个独立的线程,因此在电梯睡眠时,实际上时是在ElevatorScheduler
线程中睡眠,致使电梯睡眠和电梯调度算法运行没法并行,下降效率。
复杂度:
能够看出,调度器是比较复杂的类,而调度器中负责算法的方法又是调度器中比较复杂的方法。可是除了调度器以外,电梯类也比较复杂,这是与设计初衷不符的。缘由在上文也提到过,主要是随着代码实现的推动,电梯类的职能不断扩充,与调度类有所交叠,没有很好处理这一问题。
协做图:
这一单元的做业主要容易出bug的地方包括:
我在三次做业的强测和互测中均没有发现bug,可是个人第一次做业(整个课程的第五次做业)有一个很是严重的错误(强测和互测,因为测试机制固定,都没有检测出来):若是全部输入结束后没有马上提供输入结束信号,程序将会进入死锁,没法终止。部分代码以下:
// methods of ElevatorScheduler public synchronized void setInputEnd() { this.inputEnd = true; notifyAll(); } private synchronized void update(boolean quickRetreat) { if ( (inputEnd || quickRetreat) && buffer.isEmpty() ) { return; } while (buffer.isEmpty()) { try { wait(); /* when the thread is notified, it's still in the while loop */ } catch (InterruptedException e) { return; } } // some updates here }
可见,虽然程序退出了wait()
函数,但仍是会再次进入wait()
函数,致使死锁。一个简单的修复是在while
循环上增长一个条件;设计上的修复我在第一部分已经提到过,线程检测到TerminationRequest
后就再也不调用update
函数,把这个问题在线程内部解决。
另外一个问题是没有处理好CPU资源的让出,表如今轮询所致使的CPU_TLE
。通常来讲,线程空闲时须要等待,可以使用wait()
函数,一旦线程须要被唤醒,相应锁的notifyAll()
函数必须被调用。我在第一次电梯做业的互测中用相似如下数据的数据点发现了两个A屋的solid bug:
[5.0]1-FROM-2-TO-3 [150.0]2-FROM-14-TO-5
本单元测试程序主要考虑本身的使用,包括三部分:
输入文件的时间映射器,将带时间的文件输入映射到时间轴上,实现定时输入。具体代码见:https://github.com/YushengZhao/BUAA_OO_elevator_input_mapper
电梯仿真器,模拟电梯的真实运行,在运行过程当中检查相关问题。主要思路是:将电梯请求和被测程序输出转化成若干电梯指令,按照时间排序,在仿真器上模拟运行,在运行过程当中记录参数、检查行为,最终能够给出性能报告。
请求指令生成器,能够定制若干段请求序列,每一段能够设置参数,可参考如下代码:
def generator(size=10, timescale=10, id_start=1, time_shift=1): # generate one segment of requests pass def generate(): periods = [12,19,8,4,13] sizes = [8,4,10,13,16] s = [] id = 1 time_shift = 0.3 for i, period in enumerate(periods): s += generator(size=sizes[i],timescale=period, id_start=id,time_shift=time_shift) id += (sizes[i]+1) time_shift += (period+0.1)
将这些组件链接起来就能够生成测评机了,可是考虑到自身实际需求,就没有具体实现了。
一个值得注意的地方:许多同窗采用python脚本进行时间映射,我在参考了以前一些学长的博客以后发现,这种方法容易产生时间误差,时间控制不是很精确,而将时间映射器内嵌到Java语言内部则能够实现更精确的控制。同时,这样也便于调试。
关于请求生成策略:实际应用中可能会出现不一样时段负载不一样的状况,我在测评机中按段生成请求,能够模拟这种状况。在进行几回到几十次测试后(总请求量1e2
量级),通常没有什么显著问题;进行大量测试(1e3,1e4
量级的测试)也许能够发现一些问题,但考虑到每个测试所消耗的时间成本,就没有过多测试了。真正实际应用中,大量的测试确定是必要的。
这一单元的调试和测试与上一单元相比,主要是多了时间因素,在测试时要考虑输入随时间分布的不一样特征,如在一个时间点大量输入,在一段时间内没有输入,等等。而在调试时,因为不能使用断点调试法,我广泛采用了日志记录的方法,增长一个可插拔的logger:
private static final boolean LOGGER = true; private static final boolean ASSERTION = true; public static void log(String msg) { if (LOGGER) { System.out.println(msg); } } public static void ast(boolean condition, String msg) { // ast == assert if (ASSERTION && !condition) { System.out.println("Assertion failed: "+msg); } }
或者实现一个带level
的不一样重要性的日志输出:
private static final int LEVEL = 5; public static void log(int level, String msg) { if (level >= LEVEL) { System.out.println(msg); } }
固然,Java也有相应自带的日志记录机制,不过考虑到这一单元做业并不须要复杂的日志,就没有使用了。
多线程方面,我在作电梯第一次做业时花了比较长的时间(甚至一度觉得本身就要止步于此了),当时有许多问题想不清楚,最后实现的代码也有许多逻辑混乱的地方。多线程之因此容易出现各类安全问题,归根结底仍是线程自身行为逻辑复杂,好比,简单的生产者-消费者模型,基本不会有人写出线程安全的问题,可是复杂一些的生产者-消费者模型(如消费者在没有产品时不调用wait
函数,而是进行其余活动),就容易产生线程安全问题了。所以,根据需求创建简单通用的线程模型很是重要——简单的逻辑每每不容易出错。好比观察者模式,构建了一个线程发布消息,若干线程接收消息的模型;反过来,又有时也须要若干线程发送消息,某一个线程接收消息的状况,这时即可以采用消息队列:
class Scheduler { private Queue<Message> messageQueue; public synchronized void addMsg() {/*some code here*/} public synchronized Message getMsg() {/*some code here*/} } interface Message {} class Feedback implements Message {} interface Request extends Message {}
全部须要通知Scheduler
的消息,都经过addMsg
方法传入,不管其具体内容,以后再由其余函数分别处理。所以,当Scheduler
处理完剩余任务后,即可以直接在getMsg
方法内等待。
设计原则方面,我在完成第一次电梯做业时,并无太多的设计原则方面的意识,而因为后两次做业都沿用第一次做业的结构,全部后两次做业也没有体现多少设计原则,这是遗憾的。
代码重构方面,我从这一单元开始坚决了能不重构就不重构的态度。从计组到如今,在各类须要迭代开发的工程中,我老是感受以前写的不够好,想要重构,但又常常忽视了重构的风险以及不重构的可能性。实际开发中,重构确定是要尽可能避免的,在原来的代码(多是看似比较乱的代码)上修改其实才是常态。事实上,看似乱糟糟的代码其实可能并无想象中那么差。我在第二次电梯做业时曾经尝试了重构,可是等到真正动手写重构代码的时候才发现,若是重构,不少代码都是差很少的,原来不少设计上的考虑都是颇有道理的。
算法方面,这一单元的做业给算法留出了很大的空间。不过想拿高分并不须要很复杂的算法。最基本的look算法,在强测不出现错误的状况下,基本就能够达到95分以上了。后两次做业再加一些负载均衡的考虑,若是没有错误,基本也能拿到95分以上。尽管如此,讨论一些算法也是没有坏处的:
固然这些算法都是我没有实现的,毕竟课程的主要目的也不是算法。
其余:
Building
,给每个Building
安排若干Floor
,并提供“向上的按钮”和“向下的按钮”接口,就像现实生活中的电梯同样。电梯不知道一个楼层有多少人,只知道某一楼层有没有人想上楼、有没有人想下楼。