BUAA OO 第二单元博客总结

BUAA OO 第二单元博客总结

代码架构

在这里插入图片描述
上述图为本单元三次作业迭代之后的UML类图。

作业架构

第一次作业

其实第一次作业的架构相对来说没有什么难的点,主要是对线程的理解以及对锁的理解。首先是在Main类里面创建输入线程InputThread和控制中心ControlSystem,之后将输入的请求传入ControlSystem,由其调用RequestPool的相关方法来加入请求,然后将请求分给每一个电梯的线程。而每一个电梯的线程都需要有运行策略(LOOK策略),运行策略会根据当前RequestPool里面的请求来指导电梯下一步的动作:开门?关门?接乘客?出乘客?等。而RequestPool是共享数据,其中的方法通过synchronized的关键词修饰。

第二次作业

第二次作业相较第一次整体架构不变,但是此时乘客的电梯号需要我们自行分配,即我们需要完成相应的分配策略,然后其他的过程不变。我采取的分配策略是评估优先级的方法:根据电梯的相关参数来计算出每个电梯接该乘客的优先级程度,乘客则会被分配给优先级程度最高的乘客。然后被分配之后,就调度该电梯完成相应的请求。
其次是本次作业新增了一个电梯重置请求:重置其speed和capacity。实现的过程即在Elevator类中新添加一个方法:resetElevator()。当RequestPool接收到重置请求后,唤醒相应的电梯调用resetElevator()。需要注意的是再reset的过程中,电梯不能进行开关门、进出乘客、上下楼层、输出RECEIVE(见RECEIVE约束)等操作,处于静默状态。

第三次作业

第三次作业新增的要求是一类新的重置请求:将电梯变成双轿厢。所以,我们会在ControlSystem创建电梯线程的时候就会创建12个电梯线程(A和B),其中六个start,剩下六个在重置为双轿厢之后会通过RequestPool进行start。
其次是对双轿厢电梯操作输出的修改。对电梯和乘客增加一个新的属性type用来判断是轿厢A还是轿厢B。
最后是双轿厢防碰撞,这个本次作业需要解决的一个主要问题。我采取的实现方法在后续会讲解,这里就不过多叙述了。
然后就是分配策略会新增一个参考标准:是否是双轿厢电梯,因为双轿厢电梯的耗电量是正常电梯的1/4,所以同等条件下,双轿厢可以很好地减少耗电量,从而提高性能。

线程协作

在这里插入图片描述
上图是线程之间的协作关系:main为主线程,通过主线程去创建输入线程InputThread,并创建电梯线程。之后在RequestPool中,通过请求,去一个个调度电梯线程完成请求。

稳定和易变的内容

其实在作业架构中也有提到,这里总结一下:稳定的就是之前处于上层的部分:主线程、输入线程、控制中心。而易变的部分则是下层部分:RequestPool以及ELevator,需要对一些新的要求,新增一些新的方法或者改变一些原有的方法。例如对于双轿厢电梯的开关门输出以及乘客上下电梯的输出形式都有所改变、对新的重置请求的完成需要新增新的方法。

同步块的设置和锁的选择

本次作业中,由于我仅仅只有一个大的RequestPool属于所有电梯线程的共享数据,所以我只会在RequestPool中,对所有相关的方法通过synchronized的关键词进行修饰。这样子简单方便理解,也不需要一些更加灵活的操作,对于lock锁的使用并不需要。
在这里插入图片描述

第三次作业——双轿厢防碰撞实现

在第三次作业中,新增了双轿厢电梯功能。双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢。将楼层分为上区、下区、换乘楼层,在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层
所以在轿厢A或者轿厢B要到达换乘楼层前,需要判断另一部轿厢是否位于换乘楼层。核心思路就是:获得另一部轿厢位于的楼层,判断其是否等于换乘楼层。
所以,我在每个电梯类之中添加了一个电梯属性,用来记录另一部轿厢。
在这里插入图片描述
当轿厢想要移动到换乘楼层前,会先判断另一部是否位于换乘楼层。如果不在,则可以移动;如果在,则wait(),等待另一部轿厢离开换乘楼层后进行唤醒。
在这里插入图片描述
当另一部电梯在换乘楼层完成任务之后,我的程序会直接让其离开该换乘楼层,并唤醒另一部轿厢。
在这里插入图片描述

bug以及debug

bug

第一次作业

第一次作业因为乘客已经分配好电梯,所以只需要完成电梯的唤醒后去接乘客完成请求即可。在这个过程中,只需要给共享数据(requestPool)上锁即可。当然这样子避免了线程不安全,在第一次作业中,由于第一次接触,我会出现轮询的bug,即电梯没有任务的时候会反复调用decision的方法,导致CPU时间过长,产生CTLE的bug。

第二次作业

第二次作业难度相较第一次大大增加,首先不仅要自己完成分配策略(当然可以选择简单的分配策略,这样带来的问题也比较少);其次,更需要认真思考的是reset操作;最后是线程安全的问题。
所以在第二次作业的强侧和互测中,我也出现了两个问题的bug:一个是由于在乘客在电梯reset之后重新分配电梯没有输出receive的问题;另一个bug是迭代器遍历的问题,会出现边遍历边删除的问题。

第三次作业

第三次作业主要新增的就只有一个实现双轿厢功能。

  1. 首先是完成双轿厢reset的请求。在这个过程中会出现乘客可能分配给双轿厢的情况,这时需要判断清楚是由轿厢A还是轿厢B先来进行乘客的接送。而在后续的电梯reset重新分配的过程中,需要记得将type(表示是轿厢A还是轿厢B的变量)置零,否则可能会出现将分配给非双轿厢电梯而产生AB的问题。
  2. 其次是电梯线程结束的判断方法。在双轿厢的加入之后,乘客可能会出现换乘的可能,所以判断乘客请求结束的条件不是进入电梯,而是到达目的地,所以需要对其进行一点微调即可。
  3. 其次是双轿厢的A和B的分配移动以及防碰撞实现。而我在这次作业中犯了一个非常致命且低级的错误:提前将电梯的atFloor重置为换乘楼层的上下楼,导致我强侧寄掉了。

debug

第一次作业
  1. 首先是在初次接触线程的过程中,对于线程的notify、wait以及sleep的用法不够了解而导致的线程无法被唤醒。这个在对线程有了更深的了解之后即可解决。
  2. 其次是轮询的问题只需要在ELevator的线程中,加上一条判断,当电梯静止的时候(通过decision的方法根据当前请求得出是否静止),通过sleep来避免轮询。
    在这里插入图片描述
第二次作业
  1. 首先是关于reset操作的问题。在最初完成这个操作的过程中,出于对性能的考虑,会将原来电梯内的乘客尽可能地分配回原来的电梯。但是电梯在reset的过程中,不能开关门、进出乘客、上下楼层、输出RECEIVE(见RECEIVE约束)等操作,处于静默状态。所以会出现原来的乘客重新分配后输出的receive在reset的过程中。解决的方法就是在requestPool里面增加一个变量bufferPass(乘客缓冲池),用来记录已被分配的乘客,但是由于被分配的电梯出于reset的过程中而不能输出receive操作。在电梯reset结束之后,遍历这个容器,将属于自己电梯的乘客输出即可。
  2. 其次也是wrong_answer的bug,是由于出现了bufferPass(乘客缓冲池)之后而导致的一位乘客可能会出现两次receive的情况。对于这种wrong_answer的bug相对来说调试难度都不算很大,找到一个出错的样例,并针对出错的bug找到对应的代码段之后,详细思考代码的运行逻辑以及可能出现的细节上的问题即可。对于乘客两次receive的问题,我也是在后续根据乘客id不重复进行特判,bufferPass(乘客缓冲池)里面如果含有该乘客则不会往里面添加该乘客。
  3. 最后是一个关于线程安全的问题,也是有关迭代器遍历删除的问题。这个问题出现在乘客进出电梯的方法中,会出现下面的报错信息,这样会导致最终线程无法运行下去,而产生RTLE的问题。这个问题设计到线程安全问题,需要对相应的共享数据进行synchronized的关键词修饰,从而保证不会出现数据冲突。
    在这里插入图片描述
第三次作业
  1. 首先是在解决type的问题,只需要在乘客重新分配电梯前,使用相关方法,将type置0即可解决。
  2. 其次是导致强侧寄掉的atFloor问题,我只需要在用到atFloor的操作全都结束之后,将atFloor进行重置即可。

心得体会

  1. 第一次接触到线程,了解到了多个线程可能会同时访问或者修改一个共享数据,所以保证线程安全是多线程任务一个重中之重的任务。在本单元的三次作业中便深刻体会到了线程安全的重要性。在本单元作业中,requestPool(请求池)是属于所有电梯的一个共享的数据,里面保存着乘客请求以及电梯重置请求。当我们不对内部的方法通过synchronized的关键词进行加锁的时候,就会产生数据冲突,从而导致结果的不确定性。当然,在我们使用synchronized关键词的时候,我们还有可能会因为加锁的操作而导致死锁的问题,这样会使得两个线程之间互相锁死。所以在使用synchronized关键词的时候,我们需要思考清楚其调用的逻辑以及是否会产生死锁的问题。当然,除了通过synchronized关键词加锁之外,还可以使用lock锁来进行加锁,这样的方法更加灵活,也可以更好地满足我们的需求,但是在使用方面有着更高的要求,不像synchronized那样只需要在相应的方法或者属性前加上该关键词修饰即可。
  2. 一个好的层次化设计可以让整体代码的逻辑更加清晰,尤其在多线程作业中,可以更好的辨别出各个线程的具体情况。在本单元中,就有着Main-ControlSystem/InputThread-RequestPool/Elevator的层次结构。通过在main类里面通过ControleSystem创建RequestPool和Elevator线程,并创建InputThread线程不断读入请求。接着将请求不断加入RequestPool中,然后调用相应的线程去完成请求。这样的层次结构可以聚焦于RequestPool和Elevator,并且在修改和调试的过程中,结构清晰,可以更快确定问题所在。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值