BUAA_OO第二单元博客作业

情景回顾

实验要求

实现多电梯之间的调度以及完成增加电梯的要求。

电梯共有三类,每类都有能到达的楼层、移动速度、最大载客量等属性。

电梯运行分为三种模式

Morning : 每位乘客向电梯发送请求的时间间隔不超过2s,且起始楼层都是1楼。

Night: 所有乘客都同时向电梯发出请求,且目的楼层都时1楼。

Random: 随机情况

性能要求:用尽可能短的时间来将相应人员送至指定的楼层。

同步块的设置和锁

同步块的原理及应用

将共享对象设置为线程安全类,可以通过同步块的设置实现线程安全。

临界区上锁

当Thread-0运行到块内,Thread-1运行到临界区进入锁池,Thread-0出临界区后,自动释放list锁,Thread-1开始于其他进程竞争锁,若获得,则开始运行。

Thread-0

synchronized(list) {	
	list.test0();
//	list.notifyAll();
}

Thread-1

synchronized(list) {		
    list.test1();
//	list.notifyAll();
}

在共享对象中,对方法上锁

本质上也是对调用此方法的共享对象上锁。

class List() {
   
public synchronized void Empty() {
//	notifyAll();
	System.out.println(0);
	}

public synchronized void Empty() {
//	notifyAll();
	System.out.println(1);
	}
}

			Thread-0

list.test0();

            Thread-1

list.test1();

当Thread-0调用list.test0()时,Thread-1若调用list.test1()将会被阻塞。

本次作业锁的应用

共享对象分为两类:

waitList:所有电梯的总请求队列,将会被分配到processList

processList:每个电梯都有其相应的processList,用来记录每个电梯的处理队列。

创建线程安全类RequestList来new共享对象,通过在线程安全类的方法前加sychronized实现用锁来保证共享对象的读写正确性。

注:为避免死锁的发生,在一个sychronized方法中尽可能不要调用其他共享对象的sychronized方法。尽可能让每一个方法实现最小单元化。

调度器的设计

线程交互

本次作业中共有三类线程(类似于生产者消费者模型),通过共享对象实现交互线程的控制。

  • InDevice 输入器线程
    • 将输入的请求打进waitList共享对象中。
  • `Scheduler 调度器线程
    • waitList中的请求移入特定电梯的processList中。
  • Elevator 电梯线程
    • 将此电梯的processList中的请求移入elevatorList中(人进入此电梯),或将elevatorList中的对象移除(人出此电梯,完成此请求)。
    • elevatorList并不是共享对象,是每一个电梯线程中用来表示此电梯中的现有乘客,是其中的属性。

调度器算法分析

Scheduler线程主要实现了将waitList分配到不同电梯的processList中。

InDevice线程结束并且waitList等待队列中没有请求时,即可推出Schedule线程。

分配原则:

尽可能使各个电梯的容量比大致相同(容量比:\(processList.size() / Maxsize\)

具体算法:

定义临界容量比差值\(sli\),设其初始值为0.5;定义增量\(increase = 0.02\),每分配一个请求\(sli = sli + increase\)

遍历所有可以到达请求起始楼层的电梯,找到其中容量比最小的电梯\(e1\)和容量比最大的电梯\(e2\),分别记录其容量比为\(es1\), \(es2\)

\(es1 - es2 > sli\) ,表明电梯任务分配很不均匀,所以要把这个请求分配给\(e1\)电梯

\(e1\)电梯不能到达请求的目的楼层

找到中转楼层\(transformFloor\) ,其中\(transformFloor\)\(e1\)能到达的离该请求目的楼层最近的楼层

进行请求的拆解,\(request1 : fromFloor -> transformFloor\)\(request2 : transformFloor -> toFloor\)

寻找容量比最小的可以到达该请求目的楼层的电梯\(te\)

\(request1 -> e1.processList\), \(request2 -> te.processList\)

\(e1\)电梯呢个到达请求的目的楼层则\(request -> e1.processList\)

\(es1 - es2 <= sli\),表示电梯任务分配较为均匀

直接寻找到能到达起始楼层和目的楼层的,且容量比最轻的电梯\(de\)

\(request -> de.processList\)

注:随增请求数量的增加,过多的中转会大量浪费时间,所以设置\(increase\)参数,使得\(sli\)随着请求增加逐步变大,可以有效地避免请求中转带来的时间上的浪费。

框架设计及其可扩展性

UML类图

UML UMLmind

MainClass中新建ScheduleInDeviceElevator线程,(初始化的A、B、C三个电梯)

并在InDevice线程中创建新的Elevator(通过ADD指令创建新的电梯)

线程间的协作关系

未命名文件 (2) 线程协作

RequestList是线程安全类,waitList,processList 为共享对象

  • waitList:程序中唯一的等待队列,用来存储尚未分配的请求
    • InDevice线程将请求打入到waitList共享对象中
    • Schedule线程将waitList对象中的请求打入到某个电梯对应的processList共享对象中
  • processList:每一个电梯线程都有自己的独特的处理队列
    • Schedule线程将waitList对象中的请求打入到某个电梯对应的processList共享对象中
    • Elevator线程将自己电梯的processList中的请求打入elevatorList

可拓展性分析

Complexity Metrics

1

2

其中getRequestList()​、openClose()chooseElevator()getMain()方法结构化程度过低,过多的引用其他方法,并且有较多的循环结构。
处理这些方法是并没有较好的做到高内聚低耦合的机制,较多的函数重复地出现在同一个方法中,极有可能造成死锁或是极大的降低程序的运行速度。

Dependency Metrics

3

MainClass:主线程
RequestList:线程安全类用来生成共享对象
InDevice:输入器
Schedule:调度器
Elevator:电梯线程
Timeout:线程安全输出器
比较不错的是,各个线程之间的继承和交互还是比较清晰的,架构的可扩展性比较不错。

Bug

三次作业的强测分数大致为94,98,99

前两次作业并没有被hack到,强测也均通过。但是在第三次作业中,虽然强测通过还是被某大佬hack中,

错误的原因

RTLE(真超时问题),即电梯虽然已经将乘客全部送完,即

waitList.isEmpty() == true && processList.isEmpty() == true && elevatorList.isEmpty() == true

但是有线程没有退出,我发现这是一个随机性问题,有时AC,有时RTLE。最后发现可能是因为Schedule线程在退出前对Elevator线程的唤醒发生在了Elevator线程等待之前。导致Elevator线程一直被阻塞,无法退出,进而出现RTLE。

解决方法

Elevator线程中加入sychronized(processList)临界区,保证在Schedule线程退出并唤醒Elevator线程前,若Elevator线程正在进行如下判断

processList.isEmpty() == true && elevatorList.isEmpty() == true

则阻塞Schedule线程,直到判断完之后,再进行对Elevator线程的唤醒,全部唤醒成功后,最后退出Schedule线程,从而避免Elevator线程一直睡眠导致的RTLE。

心得体会

先说一下第二段元作业与之前的不同之处(架构分析),

特点:

值得一提的是,第二段元的输入和输出新增添了时间的维度,就需要利用管道实现定时输入,具体可以通过C语言中的#include<time.h>中的函数实现,进行测试。

重点、难点:

这次作业主要考查了对多线程的理解与应用,考察了利用共享对象的来实现多个线程之间的控制与调度。写多线程时一定要注意框架的构建,线程安全类的创建,不同线程之间的notifywait的对应关系等。

即使有了较好的构思,真正实现起来还是充满困难的,正是因为多线程运行顺序的不确定性,既便出现了bug,也很难复现,这就造成了调试的困难,对于这种线程竞争的错误较好的方法就是打日志,即在可能有问题的地方System.out.println()print大法好......),当然由于较难复现,在进行测试的时候可以写.bat批处理文件,同时对Main.jar文件进行大量的测试,通过观察日志,就可以较好地分析清楚程序出现问题的地方,进行定点的排查。若是可以复现的bug,一般是一个线程之间的函数逻辑出现了混乱,只对单一线程进行调试就好。

再谈一下单个电梯的优化思路

第五次作业用的ALS,强测分数较低,所以连夜修改成了带有主请求的伪LOOK算法,下面简单的介绍一下伪LOOK算法

  • elevatorlist.isEmpty(),即电梯为空时,找到processList\(From\)\(To\)最大,且\(floor <= getFromfloor\)的请求,将它设为最高请求。同理,\(From\)\(To\)最小,且\(floor >= getFromfloor\)的请求,将它设为最低请求。
  • 电梯运行方向向上,则最高请求为主请求,反之同理。
  • 接、送主请求时,进行捎带算法。(注意要为主请求留位置)
  • 将主请求送出后,elevatorlist.get(0)​置为主请求。
  • 直到电梯内无人时,返回第一步。(每层都要更新主请求)
  • 循环进行,直到processlist.isEmptywait电梯线程。

电梯运行的时间明显缩短了,效率有了显著的提高

最后聊一下单个电梯三种模式的优化办法

Morning:(电梯在一楼进人之后)

若人数没有达到最大容量,则等待一定的时间

  • 若在等待时间内再次来人,则让人进电梯并返回上一步
  • 若在等待时间内不在来人,则电梯直接开始运行送人
  • 若在等待时间内收到\(processList.isEnd()\),即处理队列因为等待队列终止而终止,则电梯直接开始运行送人

若达到最大容量则开始运行送人

Night:

待所有的请求进入waitList之后,按照起始楼层从高到低进行排序

正常执行即可

Random:

我们似乎一直就是在Random的技术上改良Morning和Night的哈

所以用Look算法,正常调度就好啦

同时在优化的过程中,既要遵循逻辑依从(架构清晰),又要做要到计算依从(提高性能)。

具体的优化细节就不多说啦,讲真我jiao得说的不少啦

希望此篇博文能对您有所帮助,也希望我能继续顺利的苟过下面的两个单元

posted @ 2021-04-24 20:54  qlhh  阅读(74)  评论(0编辑  收藏  举报