OO第二单元作业总结

范兴堃-22371426 学生 2024-04-19 16:57:12

OO第二单元总结

第五次作业

UML类图

img

架构设计

本次作业中我采用了Input->Manager->Elevator三级分派策略。Input线程负责接收请求,并将其放到waitQueue队列中,等待Manager线程将其分配到具体的Elevator线程。

这里的重点是RequestTable这个类。由于InputManager都要对其访问,是共享对象,且必须异步访问(否则可能出现某个请求被误删/分配两次的情况),所以必须是线程安全的。所以该类的所有方法我都设计成了同步方法。并且set类的方法——即可以改变队列状态的方法中,都加上了notifyAll以唤醒其他线程。

本次的作业指定了每个乘客要被分配到的电梯,所以调度器的功能只是按照输入进行分配,真正需要设计的是电梯线程的运行策略。

运行策略——look算法

我的Elevator类中的run方法很简洁:

    @Override
    public void run() {
        while (true) {
            Advice advice = getAdvice();
            if (advice == Advice.OVER) {
                return;
            } else if (advice == Advice.MOVE) {
                move();
            } else if (advice == Advice.REVERSE) {
                direction = -direction;
            } else if (advice == Advice.WAIT) {
                processingQueue.waitQueue();
            } else if (advice == Advice.OPEN) {
                openAndClose();
            }
        }
    }

其中Adive是一个枚举类——表示了电梯运行的所有可能情况。getAdvice方法用来检索当前电梯的运行状态与请求队列里乘客的需求,返回当前的电梯运行策略:

    public Advice getAdvice() {
        if (checkEnter() || checkExit()) {
            // 如果有人要上电梯或下电梯
            return Advice.OPEN;
        }

        if (curNum != 0) {
            // 如果电梯里有人
            return Advice.MOVE;
        }

        if (requestTable.isEmpty()) {
            // 当前请求对列中没人
            if (requestTable.isEnd()) {
                // 输入结束,所有请求处理完毕
                return Advice.OVER;
            }
            // 输入尚未结束,电梯原地等待
            return Advice.WAIT;
        } else {
            // 当前请求队列中有人
            if (arePassengersAhead()) {
                // 当前方向上有人在等
                return Advice.MOVE;
            } else {
                // 否则,翻转方向
                return Advice.REVERSE;
            }
        }
    }

这个是往届学长们大多采用的look算法,采用这种getAdvice分层设计使得代码层次清晰,便于维护——在第7次作业中我的设计里只需要稍微修改一个checkEnter函数就可以。

线程结束条件

基于我的设计里分层的考量,每个线程的结束条件都很清晰:

  1. Input:当所有的输入完毕后,就可以结束,同时给Manager发送信号,表示自己已经结束;
  2. Manager:当Input线程结束,并且请求队列中的所有请求都被分配完成后,就可以结束,并给电梯发送信号;
  3. Elevator:当Manager结束,并且自己待处理队列中的所有人都被送到目的地后,就可以结束。

第六次作业

UML类图

img

本次的架构沿用了上次的三级结构,具体实现上主要有以下两点新增:

调度策略

本次没有指派电梯了,所以要由我们在调度器里决定分配给哪个电梯。我的策略就是纯随机,也没有去卷影子电梯,但最后结果上来看随机算法的性能相当地好,可谓是零投入,高回报啊。

电梯重置

在我设计的三层结构里,都要做适当的调整:

  1. Input线程中,“请求队列”不仅代表人员的请求了,还包括重置请求。同时,其结束调试也要加上所有的重置请求也被正确地分配给调度器后;
  2. Manager线程:显而易见,重置类请求的优先级一定要比正常人员请求的优先级高,所以要特判一下优先分发重置类请求;
  3. Elevator线程:我将reset当做一种电梯的状态放到了Advice枚举类中,并设置最高优先级。

由于第一次作业中分层的设计,以及在电梯内部的自我分级设计,使得本次迭代任务比较轻松——只要把每一级自身的任务做相应的修改即可。

第七次作业

本次的架构与前面的作业中无异,类图也完全相同,主要就是内部实现的一些细节:

双轿厢电梯——如何防止电梯相撞

我将双轿厢电梯看成完全独立的两个电梯,当接受到这种重置请求时,我直接将原来的电梯线程结束,并new出来两个新电梯。

这里面就涉及几点要修改的地方:

  1. 电梯类要设置上下限,并且在checkEnter时要特判一下,防止在一个楼层打转;
  2. 对于到达不了目的地的请求,不一定不可以捎带——我们可以在上车时给每个乘客设置一个“小目标”。比如他最终想要到11层,但是我的电梯只能捎带到8层,那就让他在8层下,然后再让别的电梯捎带就行。
  3. 防止电梯在换乘楼层相撞:我就让某一个电梯永远到不了换乘楼层,但是这样如果6个电梯都重置成相同的状态可能也有问题,所以我在第一次重置时让A电梯到不了换乘楼层,第二个让B。。。——这里用一个变量上锁即可
  4. 两电梯的ID,我直接让A电梯为原来的ID+10,B为原来ID+20,在需要输出时再转化回来。

本次就是在原来的架构上修改了上面这几点,总共修改不到50行,我认为这是架构层次清晰的功劳。

简单来说,我的设计将电梯简化到了极致,防止电梯相撞的方法是让其中一个电梯永远到不了换乘层,同时控制6部电梯不会出现永远到不了某一层的情况出现。

UML协作图

img

bug分析

在这三次作业中,主要有如下几个bug:

  1. 第一次作业中为了保险,上了好几层锁,导致了死锁;
  2. 完成第二次作业过程中,出现了忙等,导致CTLE;
  3. 第二次作业中,一开始的设计里,会出现别的电梯正在reset,但是另外的电梯还在分配的情况——如果5个电梯都被reset,那剩下的电梯就会被分配一堆人,导致RTLE;
  4. 第三次作业中,由于分配的延迟,会导致重置接收和开始之间超过了两个move,我的策略是在每次move之后sleep一段时间。

debug方法

本单元debug最大的障碍在于线程运行的异步性,导致无法准确获知各线程的运行状况。

最难的部分在于出现忙等CTLE时,由于我无法准确知道哪个线程忙等了,所以一开始调试时只能靠肉眼去看——但这样显然效率极低。

后来在助教的启发下,在每个线程循环的开始处打印一些信息,如果某时刻输出了一堆信息,就说明该线程在不断轮询。如此一来,调试就变得非常轻松了。

同步块/锁的设计

在三次作业里,我都采用了同步块和同步方法进行同步处理。

其中RequestTable类是一个同步类,其所有方法都是同步的,其中会修改自身属性的方法中还加上了notifyAll以唤醒相应的进程。

代码中其他要对等待队列进行操作的地方也应对其进行上锁——主要是在电梯线程中开门关门操作里,采用同步块设计。

在我的双轿厢电梯里,由于我要控制不能让所有电梯都重置成相同的状态,所以设计了一个静态的锁对象,每次进行双轿厢重置时,进入该锁对象的同步块进行修改,判断是将上面的电梯少重置一层还是下电梯少重置一层。

我的设计比较简单,没有过多的同步设计,只在必要的地方进行了加锁,所以出现bug的几率小了很多,完成作业时每太受到这部分内容的困扰。

指标分析

img

img

img

img

img

可见,复杂度较高的类/方法主要集中在Manager和Input类里,因为我觉得这两个类的逻辑非常简单直接,所以就没有进行过度的封装。而在业务逻辑的核心Elevator类中,其代码规模是最大的,但是其复杂度反而降到很低。

评测机搭建思路

第五次作业中,我的checker的思路是先检查电梯运行行为和合法性,再把输出逆向得到一个输入序列去检验其形式的合法性——这样能非常清晰地表现出具体哪里出了bug,但是第二次作业开始加入了重置,这一评测思路就行不通了,于是就转变成了将输入输出综合看待,以输出去模拟电梯的行为,最后和输入去比对。

核心代码如下:

bool Checker::check()
{
    for (Log* log : *(parser->getLogStream())) {
        //std::cout << log->toString() << std::endl;
        Elevator* elevator = elevators[log->getElevatorId()];
        switch (log->getType()) {
            case Type::ARRIVE: {
                Arrive* a = dynamic_cast<Arrive*>(log);
                int floor = a->getCurFloor();

                // 跳跃的楼层不合法
                if (abs(floor - elevator->getCurFloor()) != 1) {
                    writeError(a->toString() + "  You jump from " + std::to_string(elevator->getCurFloor()) + " to " + std::to_string(floor));
                    return false;
                }

                // 将电梯及其内的乘客楼层更新
                elevator->setFloor(floor);

                // 判断当前是否在重置
                if (elevator->getAccept().first == true) {
                    int s = elevator->getStep();
                    if (s == 2) {
                        writeError(a->toString() + "  You moved more than 2 times after accepting reset");
                        return false;
                    }
                    elevator->setStep(s + 1);
                }
                break;
            }
                
            case Type::OPEN: {
                Open* o = dynamic_cast<Open*>(log);

                // 判断两次开门不合法
                if (elevator->getIsOpen()) {
                    writeError(o->toString() + "  You opened doors for twice");
                    return false;
                }

                elevator->setIsOpen(true);
                break;
            }                
            case Type::CLOSE: {
                Close* c = dynamic_cast<Close*>(log);

                // 判断重复关门
                if (!elevator->getIsOpen()) {
                    writeError(c->toString() + "  You closed the door when iti's already closed");
                    return false;
                }

                elevator->setIsOpen(false);
                break;
            }    
            case Type::IN: {
                In* i = dynamic_cast<In*>(log);

                int floor = i->getCurFloor();
                int passengerId = i->getPassengerId();

                // 检查上车楼层的正确性
                if (floor != elevator->getCurFloor()) {
                    writeError(i->toString() + "  The passenger got in the elevator on " + std::to_string(elevator->getCurFloor()) + "-th floor when he's actually on" + std::to_string(floor));
                    return false;
                }

                // 检查是否有这个人

                // 检查是否被分配过
                if (persons[passengerId]->getTargetElevatorId() == 0) {
                    writeError(i->toString() + "  The passenger hasn't been received");
                    return false;
                }

                // 检查是否分配正确
                if (persons[passengerId]->getTargetElevatorId() != i->getElevatorId()) {
                    writeError(i->toString() + "  The passenger gets on the wrong elevator");
                    return false;
                }

                // 检查是否已经上电梯
                if (persons[passengerId]->getIsIn()) {
                    writeError(i->toString() + "  The passenger has been in the elevator");
                    return false;
                }

                // 检查是否超载
                if (elevator->getPassengersNum() == elevator->getCapacity()) {
                    writeError(i->toString() + "  Overload!");
                    return false;
                }

                persons[passengerId]->setIsIn(true);
                elevator->addPassenger(persons[passengerId]);

                break;
            }                
            case Type::OUT: {
                Out* o = dynamic_cast<Out*>(log);

                int floor = o->getCurFloor();
                int passengerId = o->getPassengerId();

                // 检查当前是否在电梯里
                if (!elevator->hasPassenger(passengerId)) {
                    writeError(o->toString() + "  The passenger isn't in the elevator");
                    return false;
                }

                // 检查下电梯的楼层是否正确
                if (persons[passengerId]->getCurFloor() != floor) {
                    writeError(o->toString() + "  The passenger gets off on  " + std::to_string(floor) + "-th floor when he's actually on the  " + std::to_string(persons[passengerId]->getCurFloor()) + "-th");
                    return false;
                }

                elevator->removePassenger(passengerId);
                persons[passengerId]->setIsIn(false);
                persons[passengerId]->setTargetElevatorId(0);

                break;
            }                
            case Type::RECEIVE: {
                Receive* r = dynamic_cast<Receive*>(log);
                int passengerId = r->getPassengerId();
                Person* person = persons[passengerId];

                // 检查是否已经被receive了
                if (person->getTargetElevatorId() != 0) {
                    writeError(r->toString() + "  The passenger has been received");
                    return false;
                }

                person->setReceived(r->getElevatorId());

                break;
            }                
            case Type::RESET_ACCEPT: {
                Reset* r = dynamic_cast<Reset*>(log);
                
                // 检查是否已经正在重置
                if (elevator->getAccept().first == true) {
                    writeError(r->toString() + "  The reset request has been accepted");
                    return false;
                }

                elevator->setAccept(true, r->getTime());
                elevator->setReset(r);

                break;
            }
            case Type::RESET_BEGIN: {
                Reset* r = dynamic_cast<Reset*>(log);

                // 检查是否Accept
                if (elevator->getAccept().first == false) {
                    writeError(r->toString() + "  The reset request begins before accepting");
                    return false;
                }

                // 检查是否已经begin了
                if (elevator->getIsBegin()) {
                    writeError(r->toString() + "  The reset request has begun");
                    return false;
                }

                // 检查begin的时候是否还有乘客
                if (!elevator->getPassengers().empty()) {
                    writeError(r->toString() + "  When the elevator reset started, there were still passengers inside");
                    return false;
                }

                elevator->setStep(0);
                elevator->setIsBegin(true);
                for (const auto& pair : persons) {
                    Person* p = pair.second;
                    if (p->getTargetElevatorId() == r->getElevatorId()) {
                        p->setTargetElevatorId(0);
                    }
                }

                break;
            }
            case Type::RESET_END: {
                Reset* r = dynamic_cast<Reset*>(log);
                double time = r->getTime();

                // 检查是否begin
                if (!elevator->getIsBegin()) {
                    writeError(r->toString() + "  The reset request is end before beginning");
                    return false;
                }

                // 检查距离accept的时间戳是否小于5s
                if (abs(time - elevator->getAccept().second) >= 5) {
                    writeError(r->toString() + "  The gap between accept and begin is more than 5s");
                    return false;
                }

                elevator->setCapacity(elevator->getReset()->getCapacity());
                elevator->setAccept(false, 0);
                elevator->setIsBegin(false);

                break;
            } 
                
        }
    }
bool Checker::finalCheck()
{
    for (const auto& pair : persons) {
        Person* person = pair.second;

        if (person->getIsIn()) {
            writeError("Person with the id of " + std::to_string(person->getId()) + " didn't get out of the elevator in the end");
            return false;
        }

        if (!person->vertifyFloorDestination()) {
            writeError("Person with the id of " + std::to_string(person->getId()) + "didn't reach the destinantion");
            return false;
        }
    }

    for (const auto& pair : elevators) {
        // 检查是否关门
        if (pair.second->getIsOpen()) {
            writeError("The elevator with the id of " + std::to_string(pair.first) + " didn't close in the end");
            return false;
        }

        // 检查是否没有乘客
        if (!pair.second->getPassengers().empty()) {
            for (const auto& p : pair.second->getPassengers()) {
                writeError("The elevator with the id of " + std::to_string(pair.first) + " conatins the passeger with the id of " + std::to_string(p.first) + " in the end");
                return false;
            }
        }

        // 坚持是否有电梯还在reset
        if (pair.second->getIsBegin()) {
            writeError("The elevator with the id of " + std::to_string(pair.first) + " is still resetting in the end");
            return false;
        }
    }
    return true;
}

同时,由于多线程程序自动生成评测的数据不一定能覆盖到极端情况,所以设置了不同层次的接口,以满足多样化的评测需求:

img

心得体会

大名鼎鼎的电梯月终于结束了,亲身体验下来只能感叹一句电梯果然名不虚传,这一个月的体验感属实拉满了。虽然过程异常艰难,但是完成这三次迭代作业后还是有不少收获的:

  1. 初步掌握了多线程程序的编写方法,以及生产者消费者的设计模式;
  2. 对多线程程序的调试有了新的理解,比如在循环中打印信息以检查代码轮询;
  3. 体会到了架构层次的重要性。由于我一开始的设计层次就非常清晰,所以这三次迭代过程中,我的修改相对而言并没有太多,大多是在原来的基础上进行一些拓展;
  4. 线程安全的重要性:这一点在第五次作业中体现的淋漓尽致:由于刚接触多线程,对很多概念理解得还不是很清晰,一开始写的时候出现了很多该上锁的地方没有上锁,导致线程安全问题频发。可能修改一点就会让程序运行结果大不相同,此后我在写每一行代码时都很谨慎,以防出现线程安全漏洞。
...全文
94 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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