2020-面向对象-第二单元(电梯惊魂)
homework5
- 设计策略
第一次接触多线程编程,所以为保险起见,我完完全全按照生产者-消费者模式完成了这次作业。我设置了两个线程,一个输入请求线程(生产者),一个电梯线程(消费者),二者共享Channel
,Channel
即为模式中的托盘,在此处为一个等待队列。
关于电梯的调度策略,我在SSTF
策略的基础上,考虑了调头接人与当前目标花费相比是否有更大收益,以收益确定电梯运行方向。性能效果较为不错,如果我考虑清楚电梯是否会左右横跳就更好了。在强测出了一个零分bug,但最终得分有89.45。 - 自我点评
- 可取之处:较好的完成了生产者消费者模式的构建,利用这个模式,能将共享对象与功能类分离,较好地保证多线程同步互斥的正确性。在设计上,把重点放在了对生产者消费者模式的理解与运用,自认为通过第一次的练习,也较好地掌握了这一模式在多线程下的应用。
- 改进之处:电梯内部粗制滥造,导致后面只能打补丁式的修改。
- 度量工具分析
UML类图分析:
<
Statistic行数分析: 电梯和调度器(等待队列)较为复杂
Metrics复杂度分析: 电梯和调度器过于复杂,调度器可以进行拆分
协作图:电梯与Channel
交互过于复杂
homework6
- 设计策略
这次作业涉及到多部电梯的运行,所以设置了一个主调度器来将输入的请求根据一定规则发送到与各个电梯相关联的子调度器中。而相比于上次作业,只需要增加对主调度器类的编写即可,较好的复用了之前的代码。
整体框架为:Channel
为Producer
与主调度器Scheduler
共享的平台,Scheduler
再做二级生产者,将请求put
进SmallChannel
平台里,Elevator
为最终的消费者,处理请求。
电梯内部的调度算法不做改变,这里只考虑对主调度器的分配。大致思路为:即考虑电梯及其等待队列里拥有的人数,也考虑电梯此时楼层与运行方向与待分配人员的关系,根据这些定义花费函数,将待分配人员分配至花费最小的电梯那里。因为电梯都是等价的,在大部分情况下,只要让电梯都能跑起来,性能应该会较为不错。
public int calculate(SmallChannel s, Elevator e, PersonRequest p) {
int cost = 99999;
if (!s.isFull()) { cost = 9999; }
int costN = s.getNum() + e.getNum();
if (costN == 0 ) { cost = cost - 7; }
cost += costN;
int fx = p.getToFloor() - p.getFromFloor();
if ((p.getFromFloor() < e.getFloor() && e.getDirection() == -1) //the same direction
|| (p.getFromFloor() > e.getFloor() && e.getDirection() == 1)
|| (p.getFromFloor() == e.getFloor()) || e.getDirection() == 0) {
cost += Math.abs(p.getFromFloor() - e.getFloor());
if ((fx < 0 && e.getDirection() == 1) || (fx > 0 && e.getDirection() == -1)) {
cost = cost + costN + 10;
cost = cost + Math.abs(fx);
}
} else {
cost += 999;
cost = cost + Math.abs(p.getFromFloor() - e.getFloor());
if ((fx < 0 && e.getDirection() == 1) || (fx > 0 && e.getDirection() == -1)) {
cost = cost + Math.abs(fx) + costN + 10;
}
}
return cost;
}
- 自我点评
- 可取之处:再次利用生产者消费者模式,构建二级生产者消费者,层次分明,设计到同步互斥的操作均在共享对象类中完成,基本不会出现关于锁释放占用的bug。
- 改进之处:如电梯、主调度器等功能类的实现感觉不够简洁经典,与整体的生产者消费者模式相比缺少沉淀的味道。
- 度量工具分析
UML类图分析:
Statistic行数分析: 修复了第一次电梯调度的bug,其它全部造搬,只增加主调度器以及一级缓冲区。
Metrics复杂度分析:调度器二级拆分,复杂度得到下降。
协作图:列出了主线程,电梯线程与主调度器线程的时序图。
homework7
- 设计策略
这次作业新增电梯请求指令,并且电梯之间并不等价,存在需要换乘的情况。
- 为了最大限度的利用前一次作业的架构(主调度器只管人员分配,电梯只管接送人员),我建立
Person
类对PersonRequest
进行处理,Person
类持有两个PersonRequest
和一个isTransfer
标志。代码示意如下,其中对mid
楼层的选择也较为简单粗暴。(原本想建立根据当前电梯情况选择mid
楼层,但因为这样做有较多交互,并且在分配时已经做了各电梯的权衡分配,就选择了硬切割)降低了耦合,最终配合分配、调度效果也较为不错。
if(isDirect(PersonRequest)){
PersonRequest1=PersonRequest;
PersonRequest2=null;
isTransfer=false;
}else{
PersonRequest1=new PersonRequest(from,mid,id);
PersonRequest2=new PersonRequest(mid,to,id);
isTransfer=true;
}
// 选择mid
if (from < 3) {
midfloor = 1;
} else if (from > 15) {
midfloor = 15;
} else {
if (to < 3) {
midfloor = 1;
} else if (to > 15) {
midfloor = 15;
} else {
midfloor = 5;
}
}
- 换乘后的
PersonRequest2
是这次设计的最大难点,由他产生了线程结束,顺序接送的问题。下面给出我的处理:电梯类内部持有共享一级平台Channel
,在PersonRequest1
下电梯后,向Channel
放入二号请求,之后回到主调度向下分配的操作。但仅仅这样解决不了线程结束的问题,当输入线程结束时,电梯内仍有换乘请求的乘客,则它会因为主调度器线程的结束而不能往下分配。所以需要在Channel
中设置一个变量numTransfer
记录换乘乘客数量,当其为0时才能进入原来的结束条件。涉及numTransfer
的操作在两个地方,主调度器下派时,若该请求为换乘请求则numTransfer++
,在电梯线程中若下电梯的人为transfer
,则调用Channel
中的put7
。
public synchronized void put7(PersonRequest p) {
queue.add(p);
numTransfer--;
notifyAll();
}
(也考虑过在别的地方设置numTransfer
,但权衡后认为放入Channel
较好,可以方便的进行同步互斥操作,Channel
作为完全共享变量,其方法均为synchronized
)
- 自我点评
- 可取之处:将所有需要加锁操作的共享变量需置于
Channel
中,很好的封装了锁操作,在多线程上不会出现问题 - 改进之处:对设置特定监视器来加锁的操作不是很了解,也许用这种方法能简化
Channel
类。
- 度量工具分析
UML类图分析:大部分仍复用上次的类。
Statistic行数分析:
Metrics复杂度分析:主调度器,子调度器,电梯均出现飘红,因为共享Channel
变量。
协作图:电梯在三次迭代中均有小幅度细节更改。
- SOLID分析
- Single Responsibility Principle:实现的较好,输入、分配、运行均是单独的类完成,所以在迭代中多处可以视为黑箱接口使用。
- Open Close Principle:在电梯类里做的不太好,三次迭代均存在打补丁式的修改代码。
- Liscov Substitution Principle:不存在子类的设计,满足里氏替换原则。
- Interface Segregation Principle:不存在接口设计,满足接口分离原则。
- Dependency Inversion Principle:电梯抽象层次不够,有一些过程式的操作。
功能与性能的平衡方面我认为指导书中的“真正靠谱的架构,一定是可以做到兼顾正确性和性能优化的。”相当有道理。优化应该基于自己设计的架构,而不应该为了优化打破设计的内聚性。
二 、Bug and Test
Bug
自己的Bug情况
- 在第一次强测中出现了bug。在电梯调度优化中,没有全面考虑到可能出现两边均有请求且代价均小于当前代价的情况,导致电梯在两层反复上下。在修复阶段查找到这个bug时,采取以绝对值最近的请求来计算代价决定方向,很快的修复了bug。
- 我认为只要遵循生产者消费者的模式,将涉及到对共享变量的操作均置于一个Table上,可以避免因自己的疏漏导致的死锁。
互测的Bug情况
- 三次均未能找到bug。
Test
- 肉眼测试:查看关键部分逻辑是否正确,一些同步互斥操作是否合理。
- 借助同学的评测机构建了一些样例进行测试,包括指导书规定的测试要求,和超过指令数的
压力测试单纯认为指令够多才可能发现一些调度上隐藏的bug。
三、心得体会
- 线程安全:在测试中只遇到一个线程安全相关的问题,个人认为是谨慎的使用生产者消费者模式的结果。在这种经典模式下,与锁相关的操作均是肉眼可见,逻辑清晰的。利用经典的多线程模式可以很好的简化编程架构的思考,在完成架构搭建后可以让精力聚集在功能的实现上。而且线程安全因为其不确定性,有时难以复现,所以最好在编码前思考清楚哪些量是需要同步互斥的,逻辑上的自检可以有效的避免线程安全问题的出现。
- 临界区问题:主调度器中注释掉的代码可能会出现线程安全问题,因为在检查中出现对
channel
锁资源的释放,比如在检查到channel.getnumTransfer
时(还未进入),电梯线程占用channel
锁,下一个换乘的乘客,使得channel
内由空变为一名乘客且numTransfer--
变为0,释放后主调度器再检查,则发现满足条件,所有线程走向结束。而此时channel
并不为空,所以不应该结束。 - 解决方法:多个检查操作应该为原子性的,不能被打断,所以在
channel
内进行检查,这样是原子性的。
public void run() {
while (true) {
if (channel.isRealEmpty()) {
//if(channel.isEmpty()&&channel.isover()&&channel.getnumTransfer()==0)
setOver();
break;
}
distribute();
}
}
public synchronized boolean isRealEmpty() {
if (queue.isEmpty() && numTransfer == 0 && over) {
return true;
}
return false;
}
- 设计原则:在理论课上接触了SOLID原则,也在代码训练中实践了这些原则.我们在开发应该遵循经典的原则,虽然过程会比较麻烦,令人厌恶,但在真正的多次迭代开发中,这些原则会让我们的代码变得有组织有纪律,在团队间可以快速读懂使用.
- 总的来说,这三次作业应该是我真正迭代完成的,围绕着生产者消费者模式展开,已经实现并验证功能正确的代码直接复用,通过新增层次、细节处理来完成新的需求.而在性能上也一直沿用代价函数分配,SSTF加调头考虑的分配调度处理,也拿到了较好的性能分.