Ohmr的OO电梯作业
本轮OO作业是笔者接触到的第一个完整的面向对象工程,程序主要实现了模拟多电梯多用户调度运行过程。
第一次作业
第一次作业是多线程程序的入门尝试,电梯仅是一个学习框架。由于是第一次写多线程代码,遇到了许多单线程程序不会遇到的线程安全问题,主要集中在共享资源访问上,解决方法为使用关键字synchronized。
UML图
第一次作业UML图如下:
可以看到,程序一共有三个线程,输入线程 Main ,调度器线程 Scheduler,电梯线程 Elevator。其中共享资源包括
1、Main 与 Scheduler 共享的调度器上下行队列
2、Scheduler与 Elevator 共享的电梯上下行队列
在每个进程循环一轮后,使用 Thread.yield()方法放弃当前线程的时间片,避免一个线程由于加锁不当长时间占用CPU
性能分析
程序一共分为四个类,分别为 Main、Scheduler、Person、Elevator。功能分别为:
Main:程序入口,创建调度器,处理输入信息并将输入信息交给Scheduler
Scheduler:调度器类,创建多组电梯进程,将调度器队列中的请求分给各个电梯(由于本次作业只有一个电梯,故调度器类暂时体现不出其优越性)
Elevator:电梯类,接受来自调度器的请求,按照规定时间,模拟开关门、上下行、乘客进出等动作。
Person:电梯用户类,电梯类中保护用户ArrayList,记录电梯内部人员信息
对于部分Elevator类中的方法复杂度较高,包括 Elevator.run 、Elevator.judgePlan
公测
程序在公测阶段未发现任何bug。
互测
不参与互测。
反思
本次作业难度不大,主要问题均集中在多线程操作不熟练上。一段时间之后情况自然有所好转。由于时间相对充裕,笔者在此次作业后续任务打下一些铺垫,考虑了捎带的情况,同时为后续多电梯的需求做足了准备,具有较好的可扩展性。
首次面对多线程,在尚未熟练使用synchronized时,共享变量保护方面经常出现不可复现的bug。后续debug时笔者将每一次操作的信息使用标准输出打印至控制台,当bug发生时,结合控制台信息分析bug产生的位置及原因,最终解决了问题
当然,虽然笔者的程序未发现Bug,但是设计上仍有需要改进的地方:
- 更好的做法是,将输入单独作为线程,从而脱离主线程,main函数仅用作进程创建
- 未采用wait、notifyAll机制,CPU使用率较低
- synchronized对共享变量进行了很好的保护,但是有许多没必要加锁的函数也被synchronized修饰,降低了运行效率
第二次作业
第二次作业是第一次作业的升级版,需求新增捎带要求,虽然第一次作业笔者的电梯已经能够处理简单的捎带请求,但尚不足以完美应对本次作业
UML图
第二次作业UML图如下:
相较于第一次作业,结构没有太大的变化,由于仅是增添捎带需求,故主要任务是修改Elevator类的相关方法。
性能分析
可以看到,由于电梯运行复杂度的提升,Elevator类中部分方法性能较差,包括 Elevator.GoUp、Elevator.GoDown、Elevator.run、Elevator.judgePlan
公测
程序在公测阶段未发现任何bug。
互测
不参与互测。
反思
第二次作业难度依旧不大,线程数依旧是三个,轻松完成任务。主要的工作量集中在电梯捎带策略的改进上。在第一次作业中,笔者采用的捎带策略无法实现将同一时间来到电梯所在楼层的用户全部接入电梯,故笔者将开关门之间的时间间隙划分为两段,两段之间sleep一个较短的时间,将CPU释放给输入进程,从而实现接受同一时间的全部输入信息。
第二次作业笔者在课下测试过程中发现部分测试点CPU占用时间过长,主要原因是最初未使用wait、notifyall机制,在不必要的空转过程中,部分进程依然保持着对CPU的占用。当电梯队列和调度器队列均为空时,将两个线程执行wait操作,若有新的请求输入进程序,再将所有线程唤醒(notifyAll)
虽然未发现任何bug,但依然有一些值得注意或改进的点
- 将输入线程与主线程分离
- 在强测过程中,有一个点性能分为0,但笔者输出结果显示已经采用了最佳的调度策略。猜测可能是运行时间超出约定时间较多,导致性能看似不佳
第三次作业
第三次作业是多电梯调度,增加了许多新的需求,例如每个电梯有各自可达的楼层,有人数上限,有特定的运行速度等,是三次作业中工作量、难度最大的一次
UML图
第三次作业UML图如下:
从UML图可以看出,类间关系为,Main函数创建调度器线程以及输入线程,调度器通过存放的电梯列表创建三个电梯线程。没有继承关系,接口仅使用了Thread来创建进程。
共享资源包括:
- 调度器与输入线程共享调度队列
- 三部电梯与调度器共享乘客队列
- 三部电梯与调度器共享换乘队列
性能分析
程序一共分为六个类,分别为Main、Scheduler、Person、Elevator、ElevatorInput、TransferList。功能分别为:
Main:程序入口,创建调度器、输入进程
ElevatorInput:输入进程,接受输入请求,并将请求移交给调度器
Scheduler:调度器类,创建多组电梯进程,将调度器队列中的请求分给各个电梯(由于本次作业只有一个电梯,故调度器类暂时体现不出其优越性)
Elevator:电梯类,接受来自调度器的请求,按照规定时间,模拟开关门、上下行、乘客进出等动作。
Person:电梯用户类,电梯类中保护用户ArrayList,记录电梯内部人员信息
TransferList:换乘列表类,记录所有尚未完成的换乘请求
由于本次作业,请求有需要换乘的可能,故需要考虑一条请求按次序分配给两个电梯的可能,加大了Scheduler的复杂度
公测
程序在公测阶段未发现一处bug,报错信息为运行时间过长,超出限定时间。后续经过多次复现测试样例发现出现死锁,修改后未再次出现。
互测
不参与互测。
反思
第三次作业是一个六线程并发度较高的一次任务,作业的难点在于多并发带来的资源共享问题、可能的死锁问题以及换乘电梯带来的调度问题。
由于需求爆发式的增加,给我带来了较大的压力,自认为课下做了足够的测试,但依然未能发现潜在的安全问题。这次作业的主要失误在于忽略了安全性,转而追求换乘效率,对于笔者本人来说也是一个教训。
第三次作业,笔者将main函数彻底的解放出来,不再将输入进程放置在main函数里。本次作业的main函数,唯一的工作即进程创建。同时将调度器进程的创建从输入进程中解放出来,全部都由main函数实现进程创建。当然还有一些不足之处:
- 三个电梯进程的创建揉在调度器进程中,更好的做法应是在main函数中创建
- 在Elevator类中,存在部分冗余代码,应该单独列为方法进行调用,避免冗余
- 更好的做法,将共享资源单独设置为一个线程安全类,并采用单例模式进行对象创建。
时序图
三次作业时序图是大同小异的,因此在此使用第三次作业的时序图作为展示:
起始时:
main函数创建两个线程:ElevatorInput、Scheduler,继而调度器线程创建三个电梯线程,自此电梯开始正常运行
运行时:
ElevatorInput从标准读入接受请求,并将已接受到的请求交给调度器分配,调度器接受到请求后,依照起始楼层和终止楼层交给特定的Elevator,电梯在运行过程中需服务自己请求对列中的乘客,请求队列外的乘客不在服务范围内。
终止时:
ElevatorInput接受到终止信号,并将其交给调度线程。若调度队列与换乘队列均为空,则调度器线程运行结束,并将终止信号交给三部电梯。若三部电梯的请求队列均为空,则电梯线程运行结束,整个程序终止。
心得体会
这三次作业让笔者充分感受到线程安全的必要性和重要性。以第一次作业为例,由于未能保证线程安全,debug过程中出现了不可复现的bug,即共享资源ArrayList中会被插入NULL。而多线程程序无法使用idea已有的debug工具,只能逐步打印每步操作的信息,最终发现是未保证线程安全导致未能写完全。解决线程安全问题的首选方法是加锁,即synchronized,使用时还需注意,尽量对代码块进行加锁操作 ,若对整个函数加锁,执行效率很可能会降的很低。其次是使用线程安全类,例如 ArrayList 是一个非线程安全的类,与其对应的 CopyOnWriteArrayList 则是线程安全的类。当然,由于笔者对 CopyOnWriteArrayList 的了解有限,不敢直接将其应用提交到作业中,故最终使用的还是加锁的传统方法。
在整个设计过程中,我在第一次作业花了最多的时间在思考该如何布局,该如何并发操作。事实证明这一工作是非常有必要的,虽然第一次作业并不难,但是前期做好准备,对后期的需求扩展有着潜移默化的帮助。在第一次作业中,笔者就考虑到可能的需求扩展,例如:多电梯并发、电梯运行时间可变、捎带问题等等。三个线程各司其职,输入线程与调度器共享调度队列,电梯线程与调度器线程共享电梯队列,输入的请求数据逐级线性传输,这一结构从第一次作业实现之后,就未发生变动,为后续两次作业减去了许多代码重构上的麻烦。