BUAA_OO_第二单元总结

BUAA_OO_第二单元总结

第一次作业

下面是第一次作业的UML类图:
在这里插入图片描述

在第一次作业中,第一次接触多线程这个概念,从头开始是那么的不熟悉,好在是现在终于完成了作业,对多线程也有了一些了解。我主要采用了实验中提供的生产者-消费者模型:

  • 输入线程和调度线程共享一个总请求队列
  • 调度线程和六个电梯线程共享一个ArrayList列表,这个列表中有六个队列,分别对应着六个电梯的等待队列。

1.1 同步块设置与锁的选择

第一次作业的上锁方法主要是对方法进行上锁,定义一个托盘类,在这个托盘类中对方法进行上锁。

public class RequestQueue {
    private ArrayList<PersonRequest> waitqueue = new ArrayList<>();
    private int isend = 0;

	//主要的方法
    public synchronized void push(PersonRequest a) {
        //让InputThread放进去东西
        this.waitqueue.add(a);
        notifyAll();
    }

    public synchronized PersonRequest pop(int k) {
        if (this.waitqueue.size() == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果可以
        PersonRequest out = this.waitqueue.get(k);
        this.waitqueue.remove(k);
        notifyAll();
        return out;
    }
}

1.2 电梯运行策略

第一次作业中不涉及调度策略,因为已经指定了乘客要乘坐的电梯。所以只需要完成电梯运行策略即可,在本次作业中我采用ALS策略,但很遗憾在互测中,被一些边界的数据hack到了,于是在bug修复中,换成了LOOK策略,参考学长的博客:

public String getAdvice(int num, int floor, int direction, RequestQueue in, RequestQueue out) {
        if (canOpen()) { //判断是否需要开门
            return "open";
        }
        if (num != 0) { //判断电梯里是否有人。如果电梯里还有人,则沿着当前方向移动到下一层
            return "move";
        }
        else {
            if (outqueue.getlength() == 0) { //检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)
                if (outqueue.getisend() == 1) {
                    return "over";
                } else {
                    return "wait";
                }
            }

            if (OriginDirection(floor, direction)) {
                return "move"; //如果有请求发出地在电梯“前方”,则前当前方向移动一层
            } else {
                return "reverse"; //否则,电梯转向,仅状态改变
            }

        }
    }

1.3 debug策略

第一次作业中遇到的bug主要是ALS策略的超时问题,我在写的时候以为自己用的是LOOK策略,但是最后在发现互测被hack到时,发现自己写的是ALS策略,LOOK策略会优先按照当前电梯的运行方向运行下去。

  • 下面是第一次作业中遇到的bug
[1.0]100-FROM-1-TO-11-BY-1
[49.9]1-FROM-10-TO-11-BY-1
[49.9]2-FROM-9-TO-11-BY-1
[49.9]3-FROM-8-TO-11-BY-1
[49.9]4-FROM-7-TO-11-BY-1
[49.9]5-FROM-6-TO-11-BY-1
[49.9]6-FROM-5-TO-11-BY-1
……
[49.9]24-FROM-1-TO-11-BY-1
[49.9]25-FROM-1-TO-11-BY-1
[49.9]26-FROM-1-TO-11-BY-1
[49.9]27-FROM-1-TO-11-BY-1
[49.9]28-FROM-1-TO-11-BY-1
[49.9]29-FROM-1-TO-11-BY-1

这种数据会导致电梯反复的上下运行,卡时间会导致超时问题,将本次作业的电梯运行策略改成LOOK策略,成功通过bug修复,说明LOOK策略作为生活中最常见的策略,性能是很好的。

第二次作业

下面是第二次作业的UML类图:
在这里插入图片描述
其中TestMain是助教提供的一种支持带时间戳输入的类,可以进行比较方便的调试。

  • 这儿可以看到RequestQueue中新增了很多方法。这些方法主要是对得到电梯的一些性质,包括是否在RESET状态,电梯的等待队列中有多少人,这些方法基本上都使用synchronized进行修饰。
  • RequestCounter类主要是实现何时电梯才能结束,这个方法是借鉴了学长的思路以后写出来的,类似于我们os课上学到的PV操作,采用单例模式,下面是RequestCounter类中的思路。
public class RequestCounter {

    private static RequestCounter instance;
    private int cnt;

    private RequestCounter() {
        cnt = 0;
    }

    public static synchronized RequestCounter getInstance() {
        if (instance == null) {
            instance = new RequestCounter();
        }
        return instance;
    }

    public synchronized void release() {
        cnt++;
        notifyAll();
    }

    public synchronized void acquire() {
        while (true) {
            if (cnt > 0) {
                cnt -= 1;
                break;
            }
            else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.1 同步块设置与锁的选择

第二次作业中,仍然采用在第一次作业中使用的对方法上锁的方式,第二次作业相对于第一次作业的变化只有一个调度策略、和新增一个REASET指令,包括RECEIVE输出,相对来说通过锁方法的形式是可以实现的。

2.2 调度器设计与调度策略

据说这个调度策略可以采用影子电梯的方式才得到一个局部最优解,但是由于本人完成基础的作业难度已经比较大,所以在这个地方直接采用随即策略进行调度策略。

但是在课后,我也去了解了影子电梯的实现。所谓影子电梯就是进行目前电梯状态的克隆,然后把这个乘客当作最后一个乘客,分配给各个电梯,得到该乘客下电梯的时间以及电梯的耗电量。也可以看出,这个影子电梯其实是一种贪心算法。

影子电梯和真实运行的电梯的不同主要在于,真实电梯需要进行sleep(),但是影子电梯只需要对相应的参数加上一定的时间,在wait()或者到达指定楼层的时候进行参数返回。对得到的参数进行比较,将改成可分配给运行时间较短以及耗电量较小的电梯。

2.3 debug策略

这一单元的debug策略真的是太复杂了,一是经常出现复现不了出现的bug,二是复现出来的bug难以不知道是什么原因导致的,三是明明知道哪儿有bug但是不知道怎么去改,四是互测中出现的数据点太逆天

[2.1]79-FROM-1-TO-3
[2.1]33-FROM-1-TO-3
[3.5]RESET-Elevator-4-4-0.4
[3.9]RESET-Elevator-2-7-0.4
[5.1]70-FROM-1-TO-3
[5.1]36-FROM-1-TO-2
[6.2]10-FROM-1-TO-11

这一单元我出现的bug主要是REAL_TMIE_LIMIT_EXCEED的问题。

  • 在bug修复中,我不知道该怎么去解决这个无法结束的情况,因此在同学的提醒下,我对一些wait()里面加了一个参数,让这个线程定时的起来一下,可以说是“偶尔的轮询”
  • 另一个问题是线程实实在在超市的情况,这种问题一般是互测中捏造卡时间的数据点,在五个电梯都进行RESET的时候,涌入大量请求,结果只能是所有的请求都被分配给了不在RESET的电梯,结果就是所有的乘客都在这一个电梯中,导致超时。这个bug也是比较好修复的,就是给电梯设置一个乘客上限,当达到这个上限的时候就不再分配给这个电梯。

第三次作业

下面是第三次作业的UML类图:
在这里插入图片描述

因为这一次作业中引入了双轿电梯,所以我新增加了ElevatorAThread和ElevatorBThread两个类,当电梯接收到第二类RESET时,新开期两个线程,然后这个线程break掉。下面是相关代码:

		else if (advice.equals("resetreset")) {
                rereset();
                DoubleCarResetRequest a = outqueue.getsecondreset();
                outqueue.setcapacity(a.getCapacity());
                outqueue.setspeed(a.getSpeed());
                outqueue.settransferfloor(a.getTransferFloor());
                outqueue.setisdoublecar();
                int c =  a.getCapacity();
                double s = a.getSpeed();
                int f =  a.getTransferFloor();
                outqueue.setnowBfloor(f + 1);
                outqueue.setnowAfloor(f - 1);
                int eid = outqueue.getelevatorid();
                new ElevatorAThread(outqueue, eid, allqueue, f, c, s).start();
                new ElevatorBThread(outqueue, eid, allqueue, f, c, s).start();
                break;
    }

然后这两个电梯线程共享原来被终止的电梯的等待队列,这样共享的话,后面AB电梯的策略就需要更为细致的判断。

3.1 同步块设置与锁的选择

第三次作业我仍然是采用前两次的对方法进行上锁,但是不同的是,在这一次引入了双轿电梯,双轿电梯共享同一个等待队列,因此在一个电梯里面遍历的时候,需要对整个过程再锁一次对象。

	synchronized (outqueue) {
            for (int i = 0; i < outqueue.getlength();i++) {
                if (isBqueue(((PersonRequest) outqueue.getPR(i)))) {
                    int temp = caldirection(((PersonRequest) outqueue.getPR(i)).getFromFloor(),
                            ((PersonRequest) outqueue.getPR(i)).getToFloor());
                    int temp1 = shang(temp, elevatordirection);
                    if (((PersonRequest) outqueue.getPR(i)).getFromFloor() == nowfloor
                            && peoplenum < capacity && temp1 == 1) {
                        return true;
                    }
                }
            }
            outqueue.notifyAll();
        }

3.2 调度器设计与调度策略

调度策略仍然采用随机调度策略,但是这一单元分了AB轿厢,其实只需要确定分给哪个电梯就能直接确定这个乘客属于这个电梯的那个轿厢。

3.3 保证双轿厢不碰撞方法

因为在这一单元里面我对解决双轿厢电梯的方法是将原来的电梯线程结束掉,新开启两个电梯,现在的两个电梯共享原来的那个电梯的共享队列,所以可以借用RequestQueue类作为一个托盘,通过这个类来进行AB轿厢楼层的信息的获取,保证双轿厢电梯不碰撞需要注意以下几点:

  • 在RequestQueue类中新增加了几个方法:
	public synchronized void setnowAfloor(int i) {
        this.nowAfloor = i;
        notifyAll();
    }
    public synchronized int getnowAfloor() {
        notifyAll();
        return this.nowAfloor;
    }
    public synchronized void setnowBfloor(int i) {
        this.nowBfloor = i;
        notifyAll();
    }
    public synchronized int getnowBfloor() {
        notifyAll();
        return this.nowBfloor;
    }
  • 在一个电梯移动时,需要判断和该电梯在一个轨道上的另一个电梯上的楼层位置以及当前楼层的位置和移动方向:当A电梯往上走而且A电梯在转换楼层的下一层时,此时就需要等待B楼层不在转换楼层上时再移动,电梯B也同理。
	public void move() {
        if (elevatordirection == 1) {
            if (nowfloor == transferfloor - 1) {
                slep(speed);
                synchronized (outqueue) {
                    while (true) {
                        if (outqueue.getnowBfloor() != transferfloor) {
                            nowfloor++;
                            TimableOutput.println("ARRIVE-" + String.valueOf(this.nowfloor) + "-" +
                                    String.valueOf(this.elevatorid) + "-A");
                            outqueue.setnowAfloor(nowfloor);
                            break;
                        }
                        else {
                            outqueue.rewait();
                        }
                    }
                }
            }
            else {
                slep(speed);
                nowfloor++;
                TimableOutput.println("ARRIVE-" + String.valueOf(this.nowfloor) + "-" +
                        String.valueOf(this.elevatorid) + "-A");
                outqueue.setnowAfloor(nowfloor);
            }
        }
        else {
            slep(speed);
            nowfloor--;
            TimableOutput.println("ARRIVE-" + String.valueOf(this.nowfloor) + "-" +
                    String.valueOf(this.elevatorid) + "-A");
            outqueue.setnowAfloor(nowfloor);
        }
        outqueue.renotifyAll();
    }
  • 当一个电梯等待或者是结束的时候,可能在换成楼层,如果不移动的话,另一个电梯就无法达到换乘楼层了,所以需要额外判断一下。
	else if (advice.equals("wait")) {
                if (nowfloor == transferfloor) {
                    elevatordirection = 0;
                    move();
                }
                slep(2000);
            }

3.4 debug策略

这一次作业中我遇到的bug主要是CPU_TIME_LIMIT_EXCEED的问题,在中测中就遇到了很多这个bug,我认为是在给电梯建议的时候,没有列举完所有的情况,导致电梯不上也不下,就一直在转向,但是很快就能够解决了。

!!!但是在强测和互测中总共遇到了5个CPU_TIME_LIMIT_EXCEED的bug,在经过我的调试后,我发现这种问题一般发生在双轿厢电梯中,大致就是说AB共用一个等待队列,A轿厢在等待的时候,B轿厢如果在判断,那A轿厢就很容易被唤醒,然后A轿厢去判断,然后唤醒等待的B轿厢,如此下去,两个轿厢一直在不断地唤醒对方,但是我并不知道怎么去修改,于是我将wait()换成了sleep(),使得它不能被无限的唤醒。

线程架构及代码分析

4.2 最后一次作业代码复杂度

在这里插入图片描述

  • TestMain()函数是学长提供的输入接口,RequestQueue类是一个托盘类,承担着生产者往托盘的一个对象上放东西,消费者从这个对象上拿东西,里面大多数方法是用synchronized上锁的。ControlThread是调度线程,ElevatorThread,ElevatorAThread,ElevatorBThread是电梯线程类。
  • 可以看出,这次作业的方法的复杂度除了几个遍历函数以外,相对于上次来说会低一点。在几个复杂度较高的函数里,主要是含有了遍历过程,包括对等待队列以及电梯内部队列的遍历。

4.2 最后一次作业UML协作图

在这里插入图片描述

  • AllRequest是总的请求队列,输入线程往其中放入请求,调度线程从其中拿出请求分配给各个电梯。
  • 结束条件由输入线程发起,依次往调度线程和电梯线程传递。
  • 电梯线程会往总请求队列里退回请求。

心得体会

本单元的几次作业的强测分都不高,课后反思了几点原因:

  1. 第一次作业中明明知道LOOK策略的性能较好,自己也想使用LOOK策略,但是不知道怎么搞的,把ALS策略当成了LOOK策略,导致性能分很低。
  2. 第二、三次作业中要用到调度线程,但是由于本人不愿意去写较为复杂的影子电梯,直接使用随机策略,这个就具有很强的随机性了,性能分也不高。
  3. CPU_TIME_LIMIT_EXCEED的问题在课下测评机里面不知道怎么判断是否超时,不知道自己是不是会有这个问题。

总的来说,还是对多线程的了解不够多,不知道哪儿导致的死锁或者程序停不下来或者线程不断会唤醒的问题,即使bug能够被复现出来,也不知道怎么去改。每次都是面向bug的编程方式,以改完bug未完成目标,就像第三次作业中的把wait()换成sleep()的修复方式。

在经历过“电梯月”后,也能算得上是真正的入门了多线程。后来发现这个OO课程的多线程和OS课程的进程线程那一章联系很紧密,我在这个作业中判断电梯是否应该结束貌似就是使用的类似于信号量的机制,acquire()操作相当于P操作,release相当于V操作,一开始在学长博客的启发下使用了这种方法,在OS课程学到这个知识点的时候,意识到知识间真的是相互联系的。

  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值