面相对象设计与构造第二单元总结--电梯多线程

文章讲述了作者在北航计算机学院面向对象设计与构造课程中学习和实践电梯调度多线程编程的经历,涉及架构设计、同步机制、调度策略、线程安全和层次化设计的关键技术,以及遇到的挑战和解决bug的过程。
摘要由CSDN通过智能技术生成

1. 总揽

北京航空航天大学计算机学院的核心专业课——面向对象设计与构造,第二单元电梯调度多线程终于结束了!

这个单元主要考察多线程的知识,处理多线程之间的交互关系和相互写作是本单元的难点;如何将众多要注意的细节串成串全部注意到是本单元的难中难;面对多线程的debug是难中难中难。

在这三次作业当中,第一次作业我写的十分费劲,直到周六下午才基本完成,主要被卡的点是想选择一个好的架构,话花费了很多时间在想,真正开始动笔是在周四晚上了。即使如此,我基本上每一次作业也都会重构。

第二次作业适逢清明小长假,我在假期开始之前就写完了,但是没想到de了三天的bug,也是等到周六下午才通过中测。中间每一天几乎都在熬夜。

第三次作业要修改bug,在周三晚上才开始写,之后每一天白天都忙别的事,晚上开始写OO,挺痛苦的,每天都会熬到4点左右。好在周五晚上写出来了,过了中测,进了强测,成绩也比较好。

这个单元在第一次很考验我们的架构设计和熟悉java多线程的原理以及实现方法。对于多线程的运行,需要我们有很好的全局观和空间想象力。对个人的考验很大。

整个工程下来,差不多开发了1000多行代码,这是一场持续三个周的马拉松,改完bug又要进行下一次开发。对我们的体力消耗也是很大的挑战。

第二次作业迭代最痛苦,适逢清明假期,充足的假期时间更是全部用来开发,交了9次才通过中测,考验人的心理定力。

2. 同步块设置和锁的选择,以及锁与同步块中处理语句的关系

我的架构设计有几个主要的原则,其中之一就是每一对交互的线程之间必须要通过共享对象来连接。这个共享对象具有属性的数据,所有的方法都是同步的。说到同步,其实synchronized这个关键字的意思就是“同步的”,表示同时只有一个线程能够拿到这个对象锁,并且执行方法的语句。

共享对象负责线程之间的通信和交流,将共享对象内所有的对象上锁,可以起到很好的保护效果。每个时刻只有一个线程能够调用该方法,其他线程在房间外等候,等到调用完之后通过notifyall来唤醒所有正在等待的线程,可能有点笼统,但是正确性能够得到保障。

因此,我的设计实现当中锁与同步块中处理语句的关系就是完全等价的,我实现了除同步方法外,没有对任何一个对象上锁。

3. 调度器设计以及调度策略

关于调度器,我的作业里有两级调度器,一是将乘客分配给电梯的调度器,另一级是在乘客被分配给指定电梯后,电梯自身运送测略的调度器。

首先展示最后一次作业之后的UML图像:

d5698ce0574b48de8b71a21d8622711d.png

三次作业中,有稳定的内容和易变的东西,稳定的是我总体的架构,而易变的是每一种不同的调度策略和因为reset所以要改变的结束条件以及相关的架构。

3.1. 第一次作业

本次作业指定了运送乘客的电梯,因此一级调度器实现比较简单,主要难点是二级调度器,即电梯自身的运送捎带策略的选择。

3.1.1. 调度器设计与结构

我的调度器专门是一个线程,名为schedulethread,专门负责来自于inputthread线程的输入,二者之间通过requestlist进行信息的传递。第三次实验中的设计方法给了我很大的启发,如果没有请求输入,schedulelist便陷入等待状态,直到inputthread输入新的请求才被唤醒。

对于schedulethread,对接六部电梯的waittable。将每一个请求分类输入waittable中,与电梯线程通信。电梯线程在接到请求之后被唤醒开始工作,知道运送完所有乘客之后才有一次陷入等待状态。

对于线程的结束,在共享变量当中专门设置endflag,以逐级传递end的信息。线程结束在第一次作业中的实现比较容易,也是最容易的一次。

3.1.2. 调度策略

我结合能带则带,尽量不变方向的原则设计。结合往年学长学姐的博客,再结合我本人的思考,我将电梯本身看做是一个状态机。这个状态机有三种状态,分别是刚刚是休息,rest,刚刚是运动的,move,刚刚开完门,opening。状态转移函数专门由strategy类负责,根据当前状态返回指示电梯运行的指令。

如果刚刚是休息的,那么我就检测门等待楼层中的乘客,看其中是否存在等待的乘客,若同层,返回开门指令,若无,则看上下楼层是否有人,若有,返回一个向上或向下的指令。若还是没人,则继续让电梯rest。

如果电梯刚刚是move的,那么检测电梯当前楼层是否需要开门,包括里面的人想出来和外面的人想进来两种。并且若没人下电梯,并且电梯已满员,我也只能继续让电梯移动。若电梯里面有人,则要进行捎带,若电梯内无人,则决定是否要开门捎带。

如果电梯刚刚开过门,那我需要检查电梯下一步要向上走,向下走还是进入休息状态。注意这里是刚刚关过门的情况,所以此时返回的指令只能包括移动与休息,而不能再开门了,否则会无限循环下去。

3.2. 第二次作业

在第二次作业当中,我们被要求在第一次作业的基础之上添加两个功能,一是不指定电梯分配哪个乘客,二是电梯增加Reset指令。

3.2.1. 调度器策略

由于过于专注于电梯Reset指令的开发,很遗憾,这部分并没有实现高性能的影子电梯。我只是粗浅的计算了一下电梯能够接上乘客所需的时间,看看哪个电梯时间短我就选择哪个电梯。如果是在开始的时候同时到来了很对请求,当计算得到的能接到的时间相同时,我还要进行轮换,对电梯的优先级进行轮换,以保证不把所有人分配给所有的电梯。

其实就性能来说,不比影子电梯差多少。强测大概能差2分左右,个人感觉比较满意,其实这样做也更符合实际情况。

3.2.2. 调度器结构与设计

前部分如果被称为性能,那这部分一定要叫做功能。摆保证功能正确是我在本次作业中的首要任务,也是我花费大量时间的地方。

首先来回顾下我第一次作业的架构,inputthread将request放入requestlist中,schedulethread从requestlist取出request分配分类放入电梯的waittable中。电梯从waittable中取出乘客请求或重置请求并且执行。

但是需要注意,电梯只能在receive的前提下才能移动,因此必须先receive乘客再移动。我首先想到当我schedulethread在分配了之后就可以直接输出receive。但是这样会会出现一个弊端,因为电梯不能在reset的时候输出,这样也就不能将乘客分给被Reset的电梯。问题也就来了,电梯全被Reset的时候就不知道分配给哪个电梯。或者说我就只有一个电梯处于工作状态,其他电梯都在Reset,其实不一定将乘客都分配给这个电梯就是最优的选择,因此 我需要实现这样的功能:即使电梯在被Reset的时候我也能将乘客分给这部电梯,换言之,电梯在任何时刻都应该能够接受乘客。

由此,在schedulethread和电梯之间的共享对象waittable中,我多增加了一个队列,叫做unreceivedlist,在乘客被分配时首先进入这里,知道电梯在运行的时候才将乘客接受。

对于在Reset的时候需要将所有的人打回,我专门有一个线程reallocatethread负责重分配乘客。为什么我要单独开一个线程呢?如果是将被打回的乘客直接分给schedulethread,那么schedulethread会等待两个方面的输入,分别是requestlist和reallocatelist,这样就可能会在进入某一个共享对象的等待状态时若另一个来了请求,则无法唤醒。

3.3. 第三次作业

3.3.1. 上次作业电梯现状:

有一个inputthread,通过requestlist和schedulethread相连,schedulethread通过六个unreceivedlist还有一个和六个电梯相连。schedulethread还有六个电梯之间通过resetlist相互通讯协调,六个电梯通过一个reallocatelist和reallocatethread相连,每个电梯拥有自己的waittable和innerlist,并且根据当前状态来判断电梯运行行为。

确实,我在上次的电梯当中可能会出现轮询的情况。原因在于若最后一条指令为Reset,我这个电梯被标为Reset,其他电梯都在等待被重新分配,若被分配自己,则其他电梯也不会停止,会一直轮询。

现在我要往里面添加双重轿厢电梯系统,首先是将这条reset指令识别出来了之后,我应该先把它加入到unreceivedlist里面去。现在的问题就是是否要将这条Reset指令分类,这要看他们的执行逻辑是否相似。对于normalReset,在执行时,首先让电梯停下,之后赶人,然后重置电梯的基本信息。随后结束Reset状态。对于dcReset,在指定时,也是先让电梯停下,之后赶人,然后重置电梯。但这时候是将新电梯加进来。

3.3.2. 现在对于新的电梯有几种方案:

1. 在一开始就建新两个电梯,但是只启用一个,需要但是由于共享同一个请求池,确实会存在被错误唤醒的问题。

2. 或者在schedule里面建新一个电梯。我两个电梯需要有共享变量进行协作,这个感觉比较好操作。

3. 还有一种是直接在电梯线程里面创建一个线程,但是这样很不符合逻辑。

4. 刚想到一种,在一开始就新建两个电梯,他们有不同的编号,但是被联系在一起,各自有各自的全部系统,不用在别的类里面创建线程了。但是对于B电梯来说,我得先让他进行wait,这很好实现。就这么办。

所以选择第四个。

3.3.3. 电梯运行情况

修改他们的参数之后,我需要搞定请求的分配办法。

1.当一个请求到来时,我应该直接分配给对应的几号电梯的A或B,这些电梯各有一个unreceivedlist,各自运行各自的,只检测冲突即可。

2.当一个请求到来时,我直接分配给对应电梯组,电梯组公用同一个unreceivedlist和waittable。这样看起来更像是同一个线程。

选择第一个。

现在是电梯的运行阶段,当下方的A电梯向上运行时,若即将运行到共享楼层,我会先进行判断,判断这个楼层是否有电梯,若有电梯,则让上方电梯离开,然后我进入等待状态,等它离开之后,我再进入。就是说,如果我的目的地是换成楼层,我要尽快的抢占。如过我要离开,我会依依不舍的离开。

3.3.4. 电梯分配问题

还有一个问题:电梯的分配问题。

1.我需要在分配的时候就将它被分配给哪个电梯

2.我直接先把它分给电梯的序号,等到下一步再分配给具体的电梯。

3.乘客上电梯的等待时间的问题,应该的计算方法为:我遍历六个井道的电梯,这里需要重新修改,若是正常井道,则正常判断即可。但是这时候要分reset的类型。若是双轿厢电梯井道,则不能到达的电梯的楼层无限大,只计算能到电梯的时间即可。

3.3.5. 线程结束问题

对于线程的结束,现在出现了一点bug,若现在线程结束,但是有人在乘坐双轿厢电梯,这时候我不应该让线程结束才对,所以我应该还再加一条判断结束的条件,若当前所有电梯内有乘客,我就不能结束。现在的总目标就是添加一个判断,是说如果当前有电梯在工作,我就不能停。但是这样会造成轮询。所以这样的条件判断成立的时候,我应该让线程进入wait状态。但是又会出现线程最后无法唤醒的问题。

首先,若电梯进入的是unreceivedlist的等待状态,那我电梯会被schedulethread和reallocatethread唤醒,这样在功能的时候是正常的,但是无法正常结束。

如果说当前线程都不在工作,我就可以结束了,但是我线程不在工作的时候一定处于wait状态。

从更高层的角度来说,每一部电梯其实不需要知道其他电梯的工作状态,是否结束也是应该被统一调度。电梯要做的决策就单纯是关于自身的,接送乘客的问题。

4. 双轿厢怎样实现不碰撞?

在两个轿厢的电梯实现当中,必然会遇到双轿厢电梯冲突的问题。因此这两个电梯线程之间涉及到协作的问题,所以要创建一个新的对象来协调两个电梯之间的问题,我们暂且把它称为sharefloor类。

在这个类中,有一个标志位,标记着共享楼层有没有别的电梯。如果有人要来到共享楼层时,他首先检查有没有被占用,若被占用,向对方电梯发出离开的请求。对方电梯此时就会被唤醒,自己就进入等待状态,对方电梯然后执行移动的操作,并且释放标志位,再唤醒当前电梯,实现不撞车。

5. 程序出现过的bug以及debug方法

我的电梯在第一次作业的时候出现了线程死锁的问题,原因在于我想要实现在电梯开门时如果有人到来我都唤醒电梯让他进的功能,发现可能会造成电梯全部进入wait状态,无法唤醒。于是我100ms唤醒一次电梯,检测到来的乘客,让他进。

在第二次作业中,解答前文的问题,我出现了电梯如果在结束时是Reset指令,由于电梯不是同时结束,所以没有被Reset的电梯会先结束,这会导致我的分配器在重新分配时,会分配给已经结束的电梯,出问题了。

这样的一种解决方法就是若检测到电梯结束,就返回最大的时间,以保证分配器只会将乘客分配给那个刚刚结束Reset的电梯。

还有一种解决方法是让所有电梯同时结束,若发现最后一条指令是Reset指令,或者在输入结束时有电梯正在Reset,那么为了保证被重分配的乘客能够快速换乘,我所有的电梯都不要return。但这也会造成一个问题,这些正在等着的电梯并没有到来的乘客把它们唤醒了,这样就会进入wait的状态。或者不让他们wait的话,就会轮询,必须要保证他们能正常返回。

解决的方法就是延迟schedulethread发送end的时间,现在直接由schedulethread进行检测是否有重置的电梯,若有则进入等待状态等待被Reset的电梯重启,所有乘客结束输入时再发送end。

第三次作业中,由于前两次架构的成熟,比较好改。在强测中获得了不错的分数,但是胡测环节发现了一个bug,若其他电梯都被Reset,那我就会把乘客都分给没有被reset的电梯。原因在于第三次作业中我对分配策略小作修改,改错了。

6. 结语:一点心得

6.1. 线程安全

关于线程安全,只要能够保证访问时的互斥,就基本可以保证线程安全,当然更重要的还是保证对状态进行修改之后就notify,保证我正在被阻塞的线程得到唤醒。

线程安全是保证我们程序正确执行的必要的环节,如果缺少线程安全,不仅bug难以修复,在实际情况下也会造成不可逆转的灾难。所以必须构筑线程安全的坚固防线。主要实现方法就是充分利用包括synchronized关键字和读写锁在内的各种工具。

6.2. 层次化设计

关于层次化设计,分清楚谁是线程,谁是共享变量。这两者不能混为一谈。就比如说电梯线程,其实电梯当前的属性都可以从线程中抽离出来,线程只是负责电梯的开关门,而运行的依据则是电梯当前的状态,包括它的乘客等待情况。

关于共享变量之间,最好就不要有很复杂的层次结构,若有则都要加锁,显得很臃肿。两个线程之间最好只会因为等待某一个共享变量而被阻塞,否则会导致唤醒对象不一致导致无法唤醒的情况。对于线程独立操作的对象,可以具有丰富的层次结构,比如说我这次每部电梯都有waittable类,这个类又分11个楼层,每一层叫floorwaittable,每一层楼又分向上走和向下走两个方向的等待队列,三维的层次结构。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值