OO第二单元总结
oo第二单元总结
题目分析
第一次作业
题目要求:
- 多个楼座,每座一个纵向电梯
- 电梯行为:开关门、上下行、乘客进出
分析:
- 建立电梯线程Elevator和输入线程Input(选择性建立调度器线程)
- 设置电梯调度策略,判断请求是否捎带
- 管理请求队列(包括每个电梯的请求队列以及总的请求队列)
- 处理同步关系,保证线程安全
- 设置线程结束标志
第二次作业
题目要求:
- 新增横向环形电梯
- 每楼座/楼层支持多个电梯
- 支持中途新增电梯
- 请求无换乘
分析:
- 改变容器,支持多个电梯
- 增加横向电梯类
- 判断请求方向,增加调度器行为
- 对同楼座/楼层电梯设置请求分配策略(平均分配)
第三次作业
题目要求:
- 支持换乘请求
- 定制电梯速度、容量、可停靠楼座(仅对于横向电梯)
分析:
- 建立自定义请求类,进行请求分割
- 电梯构造函数支持信息自定义
- 设置换乘楼层策略(按照指导书的方法)
同步块设置与锁的选择
作业架构
第一次作业
- Input:获取输入请求,并添加至目标电梯的请求队列中;
控制电梯结束 - Elevator:电梯类,内含请求队列属性,可完成开关门、上下行、乘客进出;调度策略为ALS
- ElevatorSystem:内含五个电梯,完成电梯启动,根据编号获取目标电梯
- Queue:电梯内的请求队列,分为wait(尚未进入电梯但已被分配至该电梯)和running(已经在电梯中);可完成主请求筛选,控制电梯结束(当该请求队列为空意味着电梯可以结束)
- 其他:为方便初始化,设置默认电梯参数类DefaultParameter;
输出可能会不安全,设置输出安全类Print;
Input和ElevatorSystem只有一个实例,使用单例模式
未设置单独的调度器类,调度行为由Input与电梯的Queue直接交互进行。由于第一次作业中调度器并非必要行为,所以使用这种模式也可以勉强苟过去。程序流程如图:
同步块设置和锁的选择
本着为保护进程安全,尽可能减少共享对象的想法,本次作业的共享对象只有电梯内的请求队列Queue,对它进行操作的情况有:
- Input获取一个新的请求,并把它添加到目标电梯的请求队列中(添加到wait队列)
- Input获取的请求为空,Input结束,在结束前通知各个电梯结束
- Elevator在运行中获取Queue的结束标志,等待开关门信息
- Elevator上下人时更改wait和running队列,更新主请求
- 在电梯开门中间和移动时,Elevator进行wait,主动释放Queue的锁,并将Queue的sleep状态设置为真,保证wait时间充足,不会被Input提前notify打断
使用了synchronize对象锁,在本次作业中为保证安全大量加锁,同时在Elevator中开了一个巨大的synchronize同步锁,导致电梯本应sleep的时候只能通过wait(特定时间)完成,同时性能有所损耗
第二次作业
第二次作业其实相比第一次改动不大,只需要稍微修改一下电梯相关容器即可,所以笔者基本延续了上一次的架构
架构改变:
- Input:新增“添加电梯”行为;将查找目标电梯并添加请求的功能让渡给ElevatorSystem完成
- 新增ElevatorQueue类:电梯队列,可进行分配请求和添加电梯
- ElevatorSystem:最上层的请求分配函数
- FromAndTo:为解决横向和纵向电梯的from和to的返回类型不同设置接口来分别实现
- HoriElevator:横向电梯
- VerElevator:纵向电梯
请求分配过程:
和第一次相比主要的不同在于将挑选目标电梯的工作由Input转移至ElevatorSystem
其他:
- 均衡分配:设置环形指针,在电梯队列中每将任务分配给某个电梯,指针位置向后移动一位,保持最近获取请求的电梯最不容易再次被分配到请求(
但是貌似和自由竞争的性能并没有什么区别) - 解决不同请求的from类型不同:FromAndTo接口由Elevator和Queue分别实现,返回类型设置为int(如果请求是横向的,那么char转int)(在第三次作业的时候发现也许泛型是比转int更体面和方便的一种做法)
- 横向电梯的方法:重写了getNext()
同步块设置和锁
这次作业基本延续了上一次的架构(主要是懒)关于锁的设计也继续用了synchronize代码块,但是性能上来看lock雀食是比synchronize好一点,详细的话如果我有时间会写在下面小小总结一下
第三次作业
(第三次作业的线程协作关系基本延续了第二次作业,故不再赘述)
解决不同电梯接收的起始点类型不同——泛型
泛型就是在类里面先指定一个模板T,不直接写int或是char,这种方式有更大的可扩展性,对于不同的子类电梯(横向或是纵向),只需要继承的时候将T换成目标类型即可,不过这种方式同样带来了一些问题:
- 为了匹配Elevator的T,请求队列Queue最好也用相同的方式
- 由于T不明类型,故不能进行四则运算,只能进行简单的判断相等,想要在Elevator或是Queue中实现四则运算只能把相关运算抽象成函数等待子类重写
换乘实现:
基本是按指导书的方法实现的,为实现这个功能笔者自定义了一个MyRequest类:
- 在原始请求的一部分结束后继续将剩余请求分配出去
- 在原始请求全部完成后通知ElevatorSystem : 未解决的请求数-1
当请求全部完成后才允许电梯结束运行(否则会出现某个电梯完成了当前自己的任务而且input也结束了于是电梯关闭,但是有换乘的请求需要该电梯的情况)
程序bug
三次作业被发现的Bug
- 第一次作业强测出现一个点超时的情况,发现是因为捎带的时候不论方向是否一致,只要有人在外面等着就开门,互测出现了线程安全问题,原因是没有封装Print,导致输出不安全,时间戳非递增
- 第二次作业没有在互测和强测中发现bug
- 第三次作业经历了一次重构,重构解决了一些线程安全问题使得我通过了中测,但是在强测和互测都被hack麻了,主要是超时和错误:
- 错误体现在笔者的电梯忘记加可到达限制导致电梯可以一路扶摇直上十一楼,加了限制之后这个问题就解决了
- 超时是因为很多因素造成的,一方面是策略问题,笔者发现让横向电梯始终朝着一个方向运行要比频繁改变运行方向来得快一点;另一方面是线程安全问题,修复了线程结束标志的bug后就解决了
发现bug
- 静查
import threading
#记录电梯信息
class elevator:
def __init__(self,speed,id):
self.speed=speed
self.id=id
#记录乘客信息的文件
class file(threading.Thread):
def __init__(self, person_id, content):
super().__init__()
self.person_id=person_id #str类型
self.status = 0
self.elevator_id = -1
file_in = open(self.person_id+'.txt','w')
file_in.write(content)
file_in.close()
def run(self):
f = open(self.person_id+'.txt','a')
#遍历out.txt获取相关信息
for line in out_lines:
content = line.split('-')
temp_elevator_id = int(content[-1].replace('\n',''))
if line.__contains__('IN') and content[1]==self.person_id:
self.elevator_id=temp_elevator_id
break
for line in out_lines:
content = line.split('-')
temp_elevator_id = int(content[-1].replace('\n','')) #当前输出的电梯id
if temp_elevator_id == self.elevator_id:
if len(content) == 5 and content[1]==self.person_id:
if line.__contains__('OUT'):
self.status=0
self.elevator_id=-1
f.write(line)
if len(content)==4:
f.write(line)
#换乘电梯
elif line.__contains__('IN') and content[1]==self.person_id:
self.status=1
self.elevator_id = temp_elevator_id
f.write(line)
f.close()
file_in='stdin.txt'
file_in_obj = open(file_in,'r')
lines = file_in_obj.readlines()
file_out='out.txt'
file_out_obj=open(file_out,'r')
out_lines=file_out_obj.readlines()
person_id_list =[]
file_list = []
for line in lines:
if not line.__contains__("ADD"):
request = line.split("]")[1]
person_id = request.split('-')[0] # '_' '-' '_' '-' '_' '-'
person_id_list.append(person_id)
file_list.append(file(person_id,line))
file_list[-1].start() #启动搜索线程
写了个python程序输出每个请求的行走路线,如果两次换乘相隔时间很久的话就要考虑是不是策略有什么问题了(值得一说的是为了让这个程序跑的快一点我用了python的多线程,用魔法打败魔法)
- jconsole:判断死锁
心得体会
这单元主要是线程安全的问题,笔者为了保证安全,几乎没有改动过同步块和锁的设置,下次写多线程其实可以尝试一下不同的同步控制。另一个就是debug工具要早写好、、笔者是第三次作业输麻了之后痛定思痛开始写debug程序,这件事告诉我们不要懒惰。