Summary of Unit 2

黄瑞翔-22230611 学生 2024-04-20 19:10:59

oo第二单元作业博客(22230611-黄瑞翔


一、第五次作业

本次作业要求实现六部电梯,按需将乘客分配,直到运行结束。

UML类图

img

线程协作关系图

img

同步块的设置和锁的选择

本次作业几个线程的共享资源是总waitingList,因此在waitingList类中,需要将它的方法上锁。
在getOneAndRemove方法中,如果总表中暂时不存在request,那么需要wait。直到addRequest方法成功添加,then notifyAll()即可。
在setEnd方法中,为了保证线程正常运行和结束,需要添加notifyAll语句。

调度策略

本次作业的乘客所需电梯已经给出,因此直接分配即可。

电梯运行策略

本次作业最重要的就是电梯的运行策略,我采用的是look策略。
在我的strategy类中,通过nextDirection方法判断电梯下一步的运行方向。

int next = 0;
        ArrayList<PersonRequest> f = waitingList.getPersonRequests();
        if (!inside.isEmpty()) { //如果电梯内有人,那么仍按照原方向运行
            next = direction;
        } else {
            int p = 0;
            for (int i = 0; i < f.size(); i++) {  /*如果电梯内没人,那么遍历waitList,如果有乘客目标方向与当前电梯运行方向相同的,方向不变 */
                int z = f.get(i).getToFloor() - f.get(i).getFromFloor();
                if ((f.get(i).getFromFloor() > currentFloor && direction == 1) ||
                        (f.get(i).getFromFloor() < currentFloor && direction == -1) ||
                        (f.get(i).getFromFloor() == currentFloor && z * direction > 0)) {
                    next = direction;
                    p = 1;
                    break;
                }
            }
            if (p == 0) {
                next = -direction;  //否则,方向改变
            }
        }
        // 以下为最底层和最顶层的特殊处理
        if (currentFloor == 1 && next == -1) {
            next = 1;
        }
        if (currentFloor == 11 && next == 1) {
            next = -1;
        }
        return next;

在电梯线程中,while循环内的步骤如下:
1)判断是否return,如不,则判断是否需要wait。
2)direction = strategy.nextDirection();
3)出人、进人
4)再次判断是否return,如不,则判断是否需要wait。
5)direction = strategy.nextDirection();
6)输出arrive

bug分析

在本次作业中,强测遇到了bug,主要问题出在电梯进人时,需要利用迭代器对电梯的状态waitingList进行边遍历边删除,这时应该对这个操作加锁。否则,如果这时别的线程在向waitingList中加入元素,就会导致同步编辑的报错。

二、第六次作业

本次作业新增了reset请求,同时需要自己设计调度器将乘客合理分配给各个电梯。

UML类图

img

线程协作关系图

img

本次作业新增了ResetList类,每个电梯配备有候乘表waitingList,reset表resetList,以及两者的合表bothList。bothList元素的增添与删减都与前两者保持同步。

同步块的设置和锁的选择

在电梯线程“判断是否return,如不,则判断是否需要wait”这个步骤中,由于bothList是电梯与其他线程的主要共享资源并且是判断return或wait的重要条件,为保证线程安全,我将整个过程套上了bothList锁。

此外,在我的调度线程中,如果选择分配的电梯暂时在reset,那么我将这个请求加入该电梯的暂存数组notReceive中。在电梯reset结束后,再统一receive。在这个过程中,为避免同时读写的情况,需要将notReceive上锁。

调度策略

本次作业的调度综合考虑了多种因素,具体实现如下:

首先去掉已经达到限乘人数的电梯:

如果都已经达到限乘人数,那么直接从6部电梯中,挑选运行方向与乘客的目标方向一致,满足捎带条件的电梯:如果存在,则选择Math.abs(lifts.get(i).getCurrentFloor() - getFromFloor) + (waitingList.size() + inside.size() + notReceive.size()) * 0.6数值最小的电梯;如果不存在,则选择waitingList.size() + inside.size() + notReceive.size()最小的电梯。

如果存在没达到限乘人数的电梯,则从6部电梯中,挑选运行方向与乘客的目标方向一致,满足捎带条件的电梯:如果存在,则选择Math.abs(lifts.get(i).getCurrentFloor() - getFromFloor) + (waitingList.size() + inside.size() + notReceive.size()) * 0.6数值最小的电梯;如果不存在,则选择waitingList.size() + inside.size() + notReceive.size()最小的电梯。

bug分析

本次作业的debug过程比较漫长,bug主要集中在两个方面。

首先就是在电梯reset处理时,应将所有人从当前楼层赶出,这时应注意的是,应该将inside中的乘客放入waitingList作为缓冲,在reset过程中再将waitingList中所有请求都放回allList。如果直接放回allList,就可能导致在reset begin之前提前receive。

其次就是在调度器的结束判断条件上。由于reset的出现,在allList end之后可能仍有新请求加入其中。在改为最终版本之前,我使用了一个特殊容器,其作用是真正保留未完成的请求,并且将调度结束条件写成allList空且end并且特殊容器空,但这样以后仍会有程序结束不了情况的出现,我最后也是没能彻底解决这个问题,可能还是由于我设计的这个特殊容器考虑不够周全的缘故。最后,我将调度的结束条件改为allList end & empty,且所有电梯的ifReset均为0,才克服了这个问题。

三、第七次作业

本次作业新增了双轿厢电梯。要求如下:
双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。请思考双轿厢电梯的优势(包括双轿厢电梯耗电量优势,具体定义见性能分说明部分),合理设计调度方案。

UML类图

img

线程协作关系图

img

新增架构设计

对于双轿厢电梯电梯的加入,我在原电梯中加入了两个属性,即电梯A和电梯B。在原电梯处理DCreset请求时,我会开启两个新电梯的线程。

如何实现双轿厢的两个轿厢不碰撞

本次作业主要利用线程解决这个问题,每个电梯都有一把专用锁lock。
首先,在电梯满足移动条件时,如果下一步就走到换乘楼层并且此时另一电梯处在换乘楼层,那么就要wait。如果当前确实处于换乘楼层,那么正常运行,同时notifyAll,以唤醒处于等待的电梯。这些过程都要上锁。
其次,在电梯处于正常等待时,在这之前要判断电梯是否处在换乘楼层,如果是,那么就需要往下或往上移动一层,然后再开始正常等待。这个过程也要为lock加上锁。

调度策略

本次作业由于时间原因,没有像前一次作业那样设计一个更加合理的调度策略,采用的是均分策略。

bug分析

本次作业和前一次作业一样,遇到了不少bug。
1)在判断调度结束的条件时,我写了一个方法endJudge,用来判断所有电梯的所有表是否都空。其中,notReceive数组也考虑到了其中。但是,我将notReceive是否为空写在了整个方法的最后,所以会出现这种情况:在电梯将notReceive分发给自己或两个子电梯之前,由于还没加,所以endJudge方法判断了电梯的候乘表可能为空,但是在endJudge判断到notReceive时,己经加完了,这时notReceive就会判定为空,这会导致结束条件成立,从而提前结束。为了解决这个bug,我将notReceive是否为空移动到了前面。除此之外,endJudge中的判断顺序均须按照逻辑进行排列,才能避免提前结束的情况。
2)在电梯运行时,遇到出人的情况,应该先把人加入新队列中,然后再从原队列删除,否则也会像上面一样出现提前结束的情况。
3)在判定电梯是否结束时,我开始的操作是

if (inside.isEmpty() && bothList.isEmpty()) {
                    synchronized (allList) {
                        allList.notifyAll();
                    }
                    if (bothList.isEnd()) {
                        return;
                    }
                }    // 1
                if (inside.isEmpty() && bothList.isEmpty() //2) {
                    bothList.await();
                }

这可能导致在位置1之前判定bothList没有end,但在位置1处bothList被setEnd了,这样会导致电梯一直等下去,导致超时。所以应该在位置2处加上&& !bothList.isEnd(),这样才能避免一直等待的情况。

debug方法总结

在面对wrong_answer时,针对样例,比较容易定位到错误所在,直接改动即可;
在面对C_TLE时,可能是轮询的问题,问题大概率出在调度器中,应该在get请求时,避免无效的wait;
在面对R_TLE时,可能是由于结束不了的缘故,或是调度策略。在解决前者时,可以通过print来判断是否走到了结束这一步。

稳定与易变的内容

总的回顾这三次作业,我认为稳定的内容是总的架构、输入模式等。易变的内容是电梯自身的运行策略以及操作形式。比如,在三次作业中,对于WaitingList类中的方法、输入线程的编写、以及总的框架变化不是特别大。变更的主要是细节。特别是在增加reset后,总表的结束并不意味着真正的结束,因为后续可能还会有请求再度加入其中。这时候就需要在调度器及电梯的结束条件上加以改变。此外,调度策略也是随着要求的不同而改变的。

心得体会

在这单元的作业中,我花了很长时间与锁、等待、唤醒这一系列问题较劲。

在线程安全方面,对于锁的设置是非常必要的,越往后的共享资源越多,锁的设置就越复杂,如果设置不好,会导致很多不可复现的bug。此外,在应对轮询、持续等待等问题时,还是需要静下心来分析整个过程,及时的notifyAll处于等待的线程。再者,要合理分析if-act之间的间隙,适当的加上锁以防止条件突然变更的情况。

在层次化设计方面,本单元进一步提升了我对于层次化的认识。多线程问题,既是集中的,也是分散的。单独设计好每个线程,同时考虑清楚所有线程之间的共享资源的关系,两者都很重要。

总而言之,这单元的学习令我印象深刻,也学到了很多东西。希望在接下来的学习中,能更加顺利。

...全文
128 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧