OO2022第二单元作业总结

OO2022第二单元作业总结

三次作业同步块的设置和锁的选择

三次作业中前两次我主要用到的就是synchronized锁,第三次作业还尝试了ReentrantReadWriteLock读写锁

我加锁的地方主要集中在可能用到的共享变量上,由于我使用了生产者-消费者模式,输入线程输入请求到缓冲队列,调度器从缓冲队列中取请求,因此对缓冲队列的各种操作都加了锁。另外我的设计中,同层的横向电梯和同座的纵向电梯共享一个候乘表类,一方面调度器负责将请求从缓冲队列中取出加入到相应的候乘表,另一方面电梯内部的决策类根据当前的候乘表来进行决策,因此候乘表类也是共享变量,在候乘表中加入请求,删除请求等方法需要加锁,另外在电梯调用决策类的方法进行决策时也需要加synchronized锁(因为决策类可能会对候乘表遍历),在决策类内部就不用再关心线程安全问题,做法如下。

synchronized (waitTable) {
    goal = strategy.decide(floor, building, status);
}

在第三次作业中,由于横向电梯增加了可停靠楼座的限制,我的设计中增加了一个横向电梯的信息表,共享给所有的纵向电梯的决策类用于决策乘客在哪层下,另外调度器在取到新增纵向电梯的请求后,除了创建新的电梯线程外,也会更新这个信息表,因此也是共享变量,同样在电梯在调用决策类进行决策时需要对这个表也加锁,考虑到共享这个表的电梯数可能较多,所以尝试了读写锁(我将该读写锁直接包装成了一个类),做法如下。

在电梯调用决策类方法决策时需要获得读锁。

synchronized (waitTable) {
    ReadWriteLock.getReadLock().lock();
    goal = strategy.decide(floor, building, status);
    ReadWriteLock.getReadLock().unlock();
}

调度器在更新横向电梯信息表时则需要写锁。

ReadWriteLock.getWriteLock().lock();             
floorInfoTable.addEleInfo(eleRequest.getFloor(),eleRequest.getSwitchInfo());
ReadWriteLock.getWriteLock().unlock();

还有就是电梯在判断是否停止等待新的请求进入时,需要看候乘表是否为空,也要加synchronized锁,并在需要时wait在候乘表上。

另外官方提供的输出类不是线程安全的,也需要加锁保护。

三次作业中调度器的设计

如前所述,我使用生产者-消费者模式,第一次作业中我的调度器的作用就是从缓冲队列中取出请求,然后根据请求所在楼座将请求放入到对应电梯的候乘表中。调度器还负责通知各个电梯输入结束,具体流程是输入线程在取完请求后给缓冲队列一个结束标记,之后退出。调度器在读到缓冲队列的结束标记后,再给各个电梯的候乘表一个结束标记,然后退出。各个电梯在送完所有人后读到候乘表的结束标记后便可退出线程。

第二次作业中,我采用自由竞争策略,同楼座或者同楼层的电梯共享一个候乘表。调度器主要作用有两个,当取出的请求是新增电梯的请求时,就新创建一个新的电梯线程,而如果是乘客的请求,就将请求加入到相应的层候乘表或座候乘表中。在进程退出的交互方式上与第一次作业基本相同。

第三次作业中,调度器除了原先第二次作业的功能外,还需要支持重新分配请求。因为第三次作业需要换乘,因而电梯在将人放下后,会将请求重新加入到缓冲队列中,调度器取出请求后会进行判断,如果已经到地方就不管了,否则根据请求的出发地和目的地进行判断重新将其放到对应候乘表中。另外,在退出进程时交互方式与前两次也有差别,因为可能会出现换乘,因此在输入结束后,调度器和其他空闲的电梯都不能立即退出,我借鉴了实验课的设计方式,设计了一个检验任务完成的类,在输入结束后输入线程调用这个类对所有的乘客请求检验,调度器每判断一个请求完成,便通知检验类一个请求完成。在检验所有请求均完成后,再按照之前的流程逐层标记结束,所有线程才可以依次退出,大致做法如下。

输入线程输入结束后检查所有任务,所有任务完成后再进行标志并退出。

for (int i = 0;i < this.personReqCnt; ++i) {
    RequestCounter.getInstance().acquire();
}

requestQueue.setEnd(true);

try {
    elevatorInput.close();
} catch (IOException e) {
    e.printStackTrace();
}

调度器中判断请求到达后通知一个请求完成。

if (request.getFromFloor() == request.getToFloor() && 
    request.getFromBuilding() == request.getToBuilding()) {
    
    RequestCounter.getInstance().release();
}

检验类内部方法基本照抄实验

public synchronized void release() {
    this.count += 1;
    notifyAll();
}

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

架构模式

第三次作业UML类图

第三次作业UML协作图

架构设计

我三次作业的架构基本上架构没有大的变化,都是在前一次的基础进行增量开发。我第三次作业的总体架构设计是分为输入线程,调度器线程以及电梯线程。输入线程将请求放入到一个总的缓冲队列。调度器负责从中取出请求,根据请求种类来创建新的电梯线程或者将请求放在对应的候乘表中。同层或同座电梯共享一个候乘表,其内部策略类根据电梯内的人,候乘表,以及电梯状态位置等信息进行决策,最终给电梯返回一个目标类,包括下一个目的地,下一个状态,让谁上谁下的信息,电梯根据目标更新到下一个状态,如果有乘客下了电梯,就将新的请求重新加入缓冲队列中,调度器再取出请求重新分配。最终输入完成后输入线程调用检查类,当所有任务完成后再逐步通知各线程结束退出。

对于未来可能拓展的需求,我可以想到的就是限制竖向电梯也有停靠楼层的限制,这种情况下就需要横向电梯也共享一个竖向电梯的信息表,并更改策略类。如果电梯有新的状态,那么就更改电梯类状态机的实现。但是我这个架构中自由竞争的策略可扩展性较差,其要求同层或同座的电梯共享一个候乘表,如果后来有更加好的调度方式,改为主动分配请求的话,那么整个架构可能就需要大改。

性能设计

第一次作业中单部电梯我采用的是LOOK算法,主要就是去找最高的向下请求和最低的向上请求,中间过程中完成捎带。但是我实现中与LOOK算法稍有不同,如果电梯某时刻为空时(比如电梯里人下完了)不会继续按原方向运动,而是去找最近的最高向下或最低向上请求,这个做法后来看来其实不是特别好,在随机数据下表现与LOOK差不多,但是遇到比较特殊的数据会出现重复上下只为接某个请求的情况,导致性能较差。第一次作业中性能方面总体来说还行,在随机数据下表现不错,但是也有个别点失分较严重,原因就是上面提到的。

第二次作业中我竖向电梯还是采用跟第一次作业一样的算法,多部电梯间采用自由竞争的方法。对于横向的电梯,我采用的是类似于SSTF的算法,每次去寻找最近的请求,并且能上就上,并且优先找上电梯的请求,不考虑方向问题。最后我测试的这种算法与横向的LOOK算法性能差不多。第二次作业性能方面还是较不错的,没有什么严重的失分。

第三次作业中,可能出现跨楼层跨楼座的请求,并且横向电梯出现了限制,性能评测方面也加入了等待时间,运行时间一般最多是几秒或十几秒的差距,但是等待时间往往可能相差几十秒甚至上百秒,因此策略也可能需要进行一些调整。对于单部电梯我主要还是采用LOOK算法,但是将之前的算法改为了纯粹的LOOK算法,后来测试时二者运行时间没有什么大差别,但是纯LOOK的等待时间要远小于我原先的算法。对于多部电梯间调度,我还是采用自由竞争。

对于需要换乘的请求,我的策略是为所有竖向电梯共享一个横向电梯的可停靠信息表,由于我的电梯是其中的策略类在每个状态下进行决策,因此调度可以认为是动态拆分的。如果请求在电梯内,就找当前运行方向上距离电梯最近可以换乘的楼层,如果请求在楼层,那么就按照基准公式求当前距离最短的可换乘楼层,不过选择的楼层应该是距离出发楼层最近的作为目的楼层。我的设计中请求最多被分成三段运送,也许支持层间或座间的换乘速度可以更加提升,并且我寻找换乘楼层的策略也是相对简单的,没有考虑横向电梯速度、容量等因素,主要是考虑到这样做较为复杂,我没想出办法来实现,并且为保证正确性,就没有做。本次作业中性能方面还是较为优秀的,基本没有什么多的失分。

自己程序的Bug

我三次作业中在强测和互测中未被发现Bug。

但是第一次作业在中测时第一个点WA,并且在自己的电脑上还复现不出来,后来查出是线程结束的条件有误,导致电梯没有把人送到就结束线程了,解决办法就是增加退出的条件为候乘表和电梯内部的人均为空时。

第三次作业中,我在自测的时候发现了轮询的Bug,后来查出问题在于横向电梯有了停靠的限制,可能出现候乘表中没有其可以运送的请求,但是因为判断候乘表不空,所以就没有wait,进而出现轮询。解决方法就是修改wait的条件,将原先候乘表为空改为候乘表中没有可以运送的请求。

发现别人程序的Bug策略

我测试别人程序的Bug的方法主要就是通过自己用python编写的评测机来测试。在数据构造方面,主要就是采用的随机生成数据的方法,另外还可以随机构造一些有一定特征的数据,例如前两次作业中可以测试均在同一座的竖向请求或者均在同一层的横向请求,还有每隔一段时间产生一定量请求的数据等。在第三次作业中,构造请求集中在A和C两座,横向电梯停靠信息也均为两座间可停靠的数据来进行测试。

在对别人程序测试方面,可以采用多线程测试来提高效率,主要做法就是写一个类继承threading.Thread类,在其中run方法中调用subprocess模块进行测试即可。

在正确检查方面,主要就是采用模拟的方法,用字典维护所有电梯的位置,开关门信息以及上一次活动的时间还有人的位置、目的地等。逐条分析输出语句,判断合法性并更新信息。

我认为在数据构造方面策略与第一单元大体相同。在测试方面的不同就是第一单元的测试只需要单线程即可,但是第二单元中由于程序大部分时间都在睡眠,因此如果采用多线程方式测试可以提高测试效率。正确性检验方面,第一单元只需要调用sympy模块来检查即可,但是第二单元需要自己编写程序检验。

在第一次作业中,我发现了两个同学的Bug,都是由于输出时间戳不递增。在第二次作业中,我发现了一个同学在某些情况下会出现电梯到达0层的Bug,还有一位同学输出时间戳可能不递增,但是没有hack中。在第三次作业中,我发现了一位同学可能会出现RTLE的Bug,可能是出现死锁或者线程未及时结束等问题,具体原因不太清楚。

心得体会

本单元中我认为我的收获还是很大的,首先是学习了多线程的知识,也尝试编写了多线程的程序,同时也体会到多线程程序编写的复杂,其中需要考虑的地方很多,稍不注意很有可能出现各种线程安全方面的Bug,特别需要注意的是在共享变量的处理上,需要谨慎的进行加锁。

posted @ 2022-04-30 16:18  sicongl  阅读(14)  评论(0编辑  收藏  举报