2024-OO第二单元总结

赵楠-22373265 学生 2024-04-20 00:44:46

OO第二单元总结

第一次作业

第一次作业架构图

img

调度器设计

本次作业沿用了Training代码中Input-Schedule-Elevator的三级调度设计,共享对象使用了一个总请求队列waitQueue和六个分请求队列requestQueue,由调度器Schedule分配。Input线程从标准输出中读入后,将请求传递给waitQueue,由于第一次作业已经预先分配好了乘客乘坐的电梯,只需根据id将其分发给指定的requestQueue即可。
waitQueue为空且请求输入结束时,Schedule将六个分队列请求输入结束信号置true

电梯运行策略

我使用的是look策略。
初始设定电梯运行方向direction。当电梯运行到每一层时,如果该层有乘客的需求和电梯的运行方向相同,便搭载该名乘客;
当电梯运行方向上其他层均无乘客请求时,转向;
当电梯内无人,请求队列为空,且请求输入未结束时,等待;
当电梯内无人,等待队列为空,且请求输入结束时,退出线程。

while (true) {
            if (persons.isEmpty() && requestQueue.isEmpty() && requestQueue.isEnd()) {
                break;
            } else if (persons.isEmpty() && requestQueue.isEmpty() && !requestQueue.isEnd()) {
                requestQueue.letWait();
            } else {
                if (doPeopleWantOut()) {
                    peopleOut();
                    if (requestQueue.doPeopleWantIn(curFloor, getPeopleNum(), direction)) {
                        peopleIn();
                    }
                } else if (requestQueue.doPeopleWantIn(curFloor, getPeopleNum(), direction)) {
                    peopleIn();
                }
                if ((persons.isEmpty() && !requestQueue.isWaitingAhead(curFloor, direction))
                        || (!requestQueue.isWaitingAhead(curFloor, direction) &&
                        !doPeopleWantAhead())) {
                    direction = direction == Direction.UP ? Direction.DOWN : Direction.UP;
                }
                if (!persons.isEmpty() || !requestQueue.isEmpty()) {
                        move();
                    }
                }
            }
        }

同步块的设置和锁的选择

本次作业中我全部使用了方法锁。在共享对象RequestQueue类中,将所有的方法全部用synchronized关键字修饰,并加入了notifyAll()唤醒线程。虽然对一些只读方法来说,有些多此一举,导致一些线程无故被唤醒,但总体上较易于维护。

第二次作业

第二次作业架构图

img

新增reset请求的处理

针对本次新增的reset请求,在共享对象ResetQueue类中新增了ResetRequest属性,考虑到一个reset请求处理完成后,下一个reset请求才会到来,当Input解析到resetRequest时,直接根据其id将其传递给对应分请求队列即可。
在电梯的运行过程中,reset请求的处理为最高优先级。执行reset请求时,我的处理是在最近的层释放电梯内所有乘客,并回收该电梯请求队列中所有请求。注意若乘客若未达到目标楼层,需要新建请求对象再返回给waitQueue
需要注意的是,电梯在reset过程中,不能再被调度器分配乘客,因此需要从“RESET_BEGIN”“RESET_END”过程中,对requestQueue用同步块上锁。

public void reset() throws InterruptedException {
        if (!persons.isEmpty()) {
            peopleOut();
        }
        synchronized (requestQueue) {
            TimableOutput.println("RESET_BEGIN-" + id);
            maxLoad = requestQueue.getResetRequest().getCapacity();
            moveTime = requestQueue.getResetRequest().getSpeed();
            sleep(1200);
            TimableOutput.println("RESET_END-" + id);
            int size = requestQueue.getSize();
            for (int i = 0; i < size; i++) { //回收原请求队列
                PersonRequest personRequest = requestQueue.getOneRequestAndRemove();
                waitQueue.addRequest(personRequest);
            }
            for (int i = 0; i < getPeopleNum(); i++) {
                PersonRequest request = new PersonRequest(curFloor, persons.get(i).getToFloor(),
                        persons.get(i).getPersonId());
                waitQueue.addRequest(request);//其他人放回大队列
            }
            persons.clear();
            requestQueue.clearResetRequest();
        }
    }

调度器设计

调度器的设计上,我直接采用了随机分配的方法。从waitQueue读取一个request请求后,随机分配给六个分请求队列中的一个。这种调度方式将情况转换为了第一次作业的情况,但由于没有针对电梯当前状态进行分析,最终性能较低。其优点是对于请求队列高负载的情况下较为稳定,即使同一时刻重置五部电梯,也不会发生只分配给一个电梯的情况。
由于可能出现在最后执行reset指令的情况,Schedule还需要遍历六部电梯的reset情况,再确定将六个分队列请求输入结束信号置true,否则可能引起调度器线程提前关闭的bug

第三次作业

第三次作业架构图

img

双轿厢电梯的处理

针对本次双轿厢电梯重置请求,考虑到在电梯线程中再维护其子电梯较为复杂,因此我选择了最初开启12个电梯线程,分为AB两组。A组电梯设为初始的六部电梯。当x-A进行第二类重置前,调度器将不会给x-B电梯的请求队列中分配请求,因此其处于休眠状态;当x-A电梯进行第二类重置后,Schedule会根据乘客需求分配给x-Ax-B
对于双轿厢的两个轿厢不碰撞的方法,在开启12个电梯线程的时候,令每两个兄弟电梯都共享一个锁,每当一部双轿厢电梯的一个电梯到达共享层的相邻层时,都试图获取共享锁,只有获取共享锁后,该电梯才能进入共享层;为了处理简便,每当电梯进入共享层并完成操作后,应当立即从共享层移出。这样做可以减少两个电梯线程的信息交换,从而一定程度上规避死锁的发生。

public void move() throws InterruptedException {
        sleep((long) (moveTime * 1000));
        synchronized (middle) {
            if (curFloor == middleFloor - 1 && direction == Direction.UP) {
                if (Objects.equals(middle.getMiddleName(), "B")) {
                    return;
                } else {
                    curFloor++;
                    middle.setMiddleName("A");

                }
            } else if (curFloor == middleFloor + 1 && direction == Direction.DOWN) {
                if (Objects.equals(middle.getMiddleName(), "A")) {
                    return;
                } else {
                    curFloor--;
                    middle.setMiddleName("B");
                }
            } else if (curFloor == middleFloor) {
                curFloor = direction == Direction.UP ? curFloor + 1 : curFloor - 1;
                middle.setMiddleName(null);
            } else {
                curFloor = direction == Direction.UP ? curFloor + 1 : curFloor - 1;
            }
            TimableOutput.println("ARRIVE-" + curFloor + "-" + id + getElevatorId());
        }
    }

调度器设计

本次调度器仍然沿用了上次的随机分配。首先通过随机分配获得目标电梯id,之后若电梯为单电梯,则分配给id-A;若电梯为双电梯,则依照乘客起点的不同分配给id-Aid-B
由于双电梯的一部电梯可能无法把乘客送到终点,因此需要在共享层将乘客放出,我的做法是更新乘客起点后将其放回总请求队列,再让调度器进行完全重新分配。这样做的好处是操作较为统一,且不需要维护两个电梯的共享需求队列,完全避免了死锁的问题,缺点是性能进一步降低了,完全没有发挥出双轿厢电梯的性能优势
前两次作业中,我的Schedule中使用轮询不断从waitQueue中获取request对象。为了减少无谓的cpu时间占用,改为了wait-notify模式,当waitQueue为空时,就陷入等待即可(现在回想起来前两次是轮询居然也能通过测试,感觉离谱)
本次由于换乘机制的存在,乘客在换乘层下电梯导致waitQueue可能随时收到来自电梯的请求,使结束信号的判断比原来更为复杂,需要在原有条件基础上遍历12部电梯,确定电梯内部为空且请求队列为空,才能将12部电梯的结束信号置true。

同步块的设置和锁的选择

本次作业在许多情况下都需要将电梯的实时状态,如当前楼层等信息暴露给外界,由于担心其他线程直接访问电梯方法线程不安全,就在共享对象类RequestQueue中把电梯的一些状态量照搬了过去。每当电梯的这些状态量发生改变,就调用RequestQueuevoid synchronized setXXX()方法同步电梯的状态量。其他线程需要使用这些状态量时则调用RequestQueueint synchronized getXXX()方法获取这些量。现在想来可能有些多此一举了。

UML协作图

img

分析自己程序出现过的bug以及自己面对多线程程序的debug方法

自己程序出现过的bug

本次强测和互测未出现bug,但在完成作业的过程中出现了不少的bug。
在第一次作业中,由于没有深入理解wait-notify模式,我电梯的run方法使用了轮询,即电梯内为空且请求队列为空时,不断试图continue,导致cpu时间超时,最终经同学提醒下改为wait()解决
在第二次作业中,调度器的终止条件出现了问题。我没有考虑到reset命令在最后出现的可能,导致reset时把电梯内乘客放出后,调度器提前关闭,导致这些乘客未被分配。需要确定六个电梯都不在重置状态才能终止调度器。
第三次作业出现了更为离谱的问题,当Schedule的结束信号发出后,会把12部电梯的结束信号置true,并使12部电梯的线程退出。然而有时会出现某部电梯被notify后,却未接受到结束信号,导致其陷入永久等待的问题。当时由于实在无法解决,最终经同学提醒把wait()改为wait(500)勉强解决了问题,电梯会在陷入等待后每500ms试图判定是否满足退出条件。最后在研讨课上同学分享中说到,电梯的run方法是非安全的,因此可能在判断等待的条件和暂停语句之间发生等待的条件的改变,从而陷入永久等待。因此需要将判断等待的条件和暂停语句用同步块保护。

面对多线程程序的debug方法

在循环中,可以加入输出语句,如果短时间内进行了巨量的输出说明发生了轮询。
程序无法退出时,可以在wait()语句的两侧输出“id begin to wait”“id end wait”,从而判断陷入等待的位置。
对于我使用的随机分配方法,bug有时很难复现。对于较小数据点,可以采用偷改爆率的方式进行控制分配。
离谱的是,是否添加输出语句有极大可能决定bug是否能复现(多线程,很神奇吧)

心得体会

经过本次第二单元的学习,我对线程安全和层次化设计有了更加深入的理解。
在多个线程共享一个对象时,需要将共享对象的方法设为同步方法,或调用时用同步块包围,从而保证线程安全。尤其要注意给对象上锁的代码块内继续调用其他对象的同步方法,极易发生死锁。
层次化设计上,对生产者-消费者模型有了一定的了解,托盘的设置使得调度器和资源生产者与调用者更方便的共享对象的数据。
我在完成这一单元时,较为严格的遵循了线程安全的设计方法,架构也设计的十分简单,因此未出现死锁等问题。比较遗憾的是,虽然强测和互测未出现bug,但性能分却直接坠机了,希望下一单元继续努力。

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

301

社区成员

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

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