BUAA-OO-U2

本篇前部分讨论和大多数同学差不多,但本篇关于双箱的操作真的很6!!安利一下我的双箱操作(可直接翻阅到双箱部分食用)

第一次作业

由于笨人看到电梯的时候,对多线程一无所知,所以只能参考学长代码。以至于写完了第一次作业才知道我使用的方法叫look策略……

我整个同步块模型框架基本上在迭代中都没有变,因此就直接在第一次作业的地方介绍了:

大致思路为一个最最基本的生产者消费者模型。一共两个线程,其中请求输入类作为生产者线程,电梯类作为消费者线程,调度器作为共享变量管理请求队列。作为生产者,请求输入类会将请求加入到调度器的请求队列中,电梯类则作为消费者根据请求队列中的请求进行运行,并在相应层将人加入到电梯当中,成为电梯队列的一元。而对于池子类,我们只需要在向池子里塞人删人的时候需要同步更新,只需要包装好就可以了。

关于调度算法,我只根据请求队列里的请求以及电梯内的人的请求来对电梯运行的方向进行转换。当电梯和请求队列中都没有人时,电梯类的 direction 变量置为0,并 wait() 。当电梯暂停,即为 direction 变量为0时,根据当前请求列表的第一个请求的所在楼层,改变电梯的方向,即 direction 的值,以使其向请求所在楼层运行。在电梯运行过程中,只有当前运行方向上仍有请求队列中请求的所在层,或者有当前电梯中的人的目标层,那么便不改变运行方向,否则使电梯反向运行或停止。每当电梯在运行时经过一层,都会扫描电梯内部有无要出去的人,或者当前层需要进电梯的人。只要二者满足其一便开门,出人进人,关门。全程只有两个锁。

第一次作业还是比较基础的,由于善良的课程组规定了必须上的电梯的编号,所以没有什么策略可言,就老老实实按编号放就行了。

(可以看到这个时候的Elevator长得还很可爱)

第二次作业

第二次作业的迭代我仔细研究了一下,其实如果在电梯之前就必须输出RECEIVE的话,那么自由竞争就被ban了。

那么我们来讨论一下剩下的主流分配方法:

  • 影子电梯:

    • 方法:

      模拟运行,把request都放到每个电梯里面试运行一下,放到所需时长最短的电梯里。

    • 优劣:

      除了自由竞争以外的最佳方法。但实际使用起来有很多问题,比如仍然不能预知下一个操作,运算时间长,而且还占CPU运行时间(我认识一哥,不知道是不是因为写影子电梯导致CTLE了,但改成随机数之后就过了,回来跟我们破口大骂影子电梯)。

  • 系数分配:

    • 方法:

      给不同的参数设置比重,比如电梯内人数、电梯耗电量、电梯里目标的距离

    • 优劣:

      看似相对优的方法,而实操难度不必影子电梯小,反而性价比要比影子电梯低不少。尤其是各类参数的设置,感觉完全是唯心主义的分配啊,有点担心被恶意构造数据hack

  • 随机数:

    • 方法:

      字面意思,摇号

    • 优劣:

      我愿称为最佳方法,天然防hack。唯一的缺点是bug无法复现。但bug无法复现有什么影响?我只需要一直摇号摇到没bug为止!

总上,还是直接摇随机数最方便省事(性价比最高口牙!)。因此,如果一开始就打定随机数的想法的话,那么这次作业非常简单(然而很快我就会因为这个认知而后悔)

关于reset,理论上很简单,假如有reset指令就停下电梯,把电梯里的人都吐出来,并且和电梯对应的小池子里面的操作一起都重新加到大池子里面。但在实际操作中还是会遇到很多问题,比如一个经典数据:假如reset是最后一个指令而且被reset的电梯里面还有人的话,那么该电梯里面的人肯定还要被再吐出来,但是此时已经把各个池子setEnd了,放不回去了。

但实际上,所谓面向对象编程,那我们一定要详细研究一下数据(bushi)。输出在50s之前结束,而时限是120s。也就是说,我们只需要让程序“等一等”再结束就好了呀。因此简单粗暴,输出结束后50s再setEnd就可以了。关于等待时间的问题,我没想那么多,直接用最劣情况,而在实操中,和同样方法的同学交流了下,他只等了20s,也可以过。而如果担心性能分,实际上在评测中的绝大部分样例都至少80s,所以等待根本不会对性能造成任何伤害。

然而因为reset的存在,导致摇随机数会出现一个显而易见的问题:假如现在有五个电梯都是reset中,那么我会从未reset的电梯里面要随机数,也即,现在所有的操作都分配到唯一那个未reset的电梯。

但只有互测出现了这个问题,强测里面,这个摇随机数的性能分几乎都是98、99,比我想象中的性能要好不少。

其实从第二次作业开始,Elevator的臃肿就初见端倪了,我还思考了很久要不要写一个策略类,后面会解释。

第三次作业

第三次作业我愿称为补丁火葬场。

补丁一:

我第二次作业的时候等待时间设置的太长了,导致性能分收到了影响(实际上改短一点就能解决的问题),我就重新在schedule里面的写了一把锁,在第一次作业中,当输出结束就可以把大池子和小池子setEnd了,但是由于reset的存在,在最后一个人结束前我们都不知道会不会有人因为reset而回到大池子中。所以这里面要锁一下schedule,当输出结束的时候先wait(),只有当所有操作都结束的时候再唤醒schedule,结束进程。

补丁二:

因为怕双箱电梯来回搬运导致超时,我不得不写了个影子电梯(那我为什么不第二次就写完了呢啊啊啊啊啊啊??)

关于处理changfloor的问题我会在后面说,虽然我的那个操作很蠢,但不得不说确实把我整个思考难度给降下来了,主要让所有关于电梯的操作都能“按电梯分人头”,在电梯的所有操作中,都需要把当前电梯的当前箱子的inlist、小池子、floor、direction之类的变量都传进去。还好我之前懒没写策略类,不然现在又要火葬场了。但代价就是,会有海量的变量和变量的赋值、重构,导致代码长度直线上升。如下,我的Elevator类里面有无数丑陋的变量。

没有策略类的代价就是Elevator的聚合度超表。唉。不过还好,第三次作业的迭代没有涉及到锁,只需要打补丁就好了。

《巨人Elevator和它的附属小弟们》

但其实Elevator里面一大半的函数都只是在单纯的引用变量、给变量赋值……只能说明它臃肿且无用。

和第五次作业相比,其实RequestPool和schedule这两个类几乎都没变,只有Elevator在不停不停地扩张。

迭代中的异变和不变

正如前文所言,只要把池子类封装好了,其实根本就没有任何变化。变化的只有Elevator类。

如何控制双箱(本篇最精华部分,推荐阅读!)

关于这个的解释,不如解释一下我诡异的双箱实现。我之前以为这个方法很蠢,但是交流了一下之后,发现简直是天才的操作。

先解释一下运行思路:我在电梯的属性里面加了一条判断当前是否是双箱,如果是双箱,就同时运行a和b,否则的话就按照普通电梯运行。样例如下

 if (isAB) {
     aGetIn(inlistA,requestPoolA,directionA,floorA);
     bGetIn(inlistB,requestPoolB,directionB,floorB);
 } else {
     getIn(inlist,requestPool,direction,floor);
 }

再详细解释一下为什么这个想法很天才:

我们想一想,每个电梯相当于一个线程,但是双箱电梯是一个线程还是两个线程呢?

只要是一个电梯,就可以算作一个线程!只不过,在双箱电梯里面,一个线程跑两部电梯就可以了!

大家可能觉得很匪夷所思,但是让我们来算一笔账:虽然我们的课程叫面向对象,但不一定要真的面向对象;同样,虽然有两部电梯,但不一定要真的分两个线程;我吃了两碗粉,但只给了一碗的钱,公不公平?公平!

该想法直接导致本题难度直线下降,我只需要机械地把每份变量都存三份,普通电梯、A电梯、B电梯就可以了。不过有得必有所失,直接导致了每个电梯的变量多到爆炸。

该想法最大的好处其实在于控制changeFloor。大家的想法一般是抢锁、实时更新楼层、电梯互相引用,而这些操作都不可避免地会涉及到线程安全。但是如果两个电梯在一个线程里,这还有个锤子的线程安全,跑就完了。控制两个箱子不能同时在changFloor就变得特别简单了,只需要在move的时候判断一下两个箱子是否一上一下或者是否停在changFloor里

 if (isAb) {
             directionA = findDirection(rpA,inlistA,floorA);
             directionB = findDirection(rpB,inlistB,floorB);
             if (directionA == 0 && directionB == 0) {
                 waiting();
             } else if (directionA == 1) {
                 floorA++;
                 sleep(fullTime);
                 if ((directionB == 0 && floorB == cf) || directionB == 1) {
                     floorB++;
                 } else if (directionB == -1 && !(floorB == cf + 1 && floorA == cf)) {
                     floorB--;
                 }
             } else if (directionB == -1) {
                 floorB--;
                 sleep(fullTime);
                 if ((directionA == 0 && floorA == cf) || directionA == -1) {
                     floorA--;
                 }
             } else {
                 sleep(fullTime);
                 floorA += directionA;
                 floorB += directionB;
             }
         } else {
             direction = findDirection(requestPool,inlist,floor);
             if (direction == 0) {
                 waiting();
             } else {
                 floor += direction;
                 sleep(fullTime);
             }
         }

而且除了简单和减少线程安全隐患之外还有一些别的好处,比如影子电梯的话可以减少算法时间并且减少克隆过程,减少对象类(可以不用建新的双箱类),减少线程切换时间等等。

实际上我们会发现,把两个电梯箱子分开,然后抢锁并没有什么好处,只会徒增重构量。事实上,不会堆屎山的程序员不是好程序员,不能打补丁的代码不是好代码(bushi)

所以,第三次迭代难吗?如难!

BUG分析

我出现过的bug

首先就是经典的电梯不停诡异事件。归根结底还是判断的结束条件不对,没有在该唤醒的时候把线程唤醒。但这种bug真的挺难de的,因为不像第一单元可以调试,第二单元就只能输出加瞪眼了……

第二个就是前面提到过的五个电梯reset导致我只能往最后一个电梯里塞的事情。这个bug其实很好解决,我只需要在电梯里面再添加一个小池子,让电梯在reset的时候可以往这个新的小池子里面塞人,但这个时候不输出RECEIVE。而当reset结束的时候,我们重新把新小池子里面的request放回原先的requestPool中,并在放回的过程中RECEIVE就可以了。

debug方法

首先,由于没法调试,所以在找到是哪部分出了问题的时候就很难找。我的建议是先进行“bug化简”和“bug复现”。怎么说呢,就是当有一大推数据输进来产生bug的时候,先试着让这些数据简化,而能继续卡住程序的bug。当化简到再化简就没有bug的时候就完成了化简。其实到这一步的时候,已经能大致猜到是哪部分出的问题了,如果还是不知道的话,就可以采用“bug复现”,试着自己再构造几个数据,尽量短而且还能卡出来同样的bug。当复现之后就基本上能找到出问题的地方了。

然后就是关于电梯停不停的卡bug,我的方法是在wait()前后都分别输出“Start-Wait-id”和“End-Wait-id”,以便更好地找到出bug的地方。

心得体会

这单元主要是多线程安全的训练,但我在写程序的过程中并没有太多遇到线程安全的问题,甚至写代码的有效性要比上一单元高。我的理解是一定要想清楚你线程运行的时序,什么时候停,什么时候唤醒,由谁来唤醒,这些问题想好之后,其他的事情迎刃而解。此外在写下每一个共享对象时都在脑子里过一遍这个对象在其他线程中可能会进行哪些操作,将其依照时序排列组合一番,确定自己正在写下的操作在任何一种情况下都不会出问题才能放心写下,这正是我在进行迭代修改的过程中所做的,根据所要新加的功能整体过一遍代码,过完也就改完了。

  • 13
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值