OO第二单元作业总结

OO第二单元作业总结

一、设计架构

1.大致介绍

在这里插入图片描述

这是我的第一次作业设计,设计主体为电梯线程EleThread和调度器Scheduler,其设计核心思想是贪心分配,以达到等待时间短、耗电量少的目标。相对地,由于贪心未必能达到全局最优,甚至可能导致电梯分配极度不均匀,因而运行速度算不上快。


在第二次作业的迭代中,由于迭代出的程序运行太慢,担心会在强测中RTLE,于是我抛弃了这一版设计,改为了自由竞争,以追求速度,避免RTLE。其延续了之前的大部分类,只去除了调度器Scheduler,并把电梯线程EleThread的运行逻辑做了更改。


在第三次作业的迭代中,依旧使用第二次作业的自由竞争设计。对于换乘,在方法类Factory中增加了Dijkstra算法来将乘客的路径分成若干段,每次取出第一段由电梯判断是否可以接客。


2.同步块与锁

在第一次作业的设计中,调度器在没有输入时需要进入等待状态以避免轮询,直到输入线程输入;电梯线程也需要在待接队列为空时进入等待状态,直到调度器分配乘客。
调度器的乘客队列、每个电梯自己的乘客队列需要加锁,以避免输入线程和调度器的冲突、调度器和电梯的冲突。具体为:调度器分配乘客时,需要将乘客队列加锁,以避免此时调度器分配乘客(移除数组),而输入线程增加乘客导致出现bug。调度器向每个电梯的待接乘客队列分配乘客时同理。


在第二、三次作业设计中均采用自由竞争策略,因而电梯需要在接取乘客时,对该层的乘客数组加锁,避免产生冲突。在输入线程向乘客队列中增加乘客时,也需要加锁来避免可能的bug。
而由于我的设计的特殊性,则不需要使用wait来避免轮询。

二、开发历程

1.第一次作业

第一次作业中我采用了调度器——电梯的设计模式,分配策略为局部贪心策略。

调度器在接收到乘客后,存储到自身的队列中,然后将其分配到每个电梯自身的待接队列waitList中,电梯按照待接队列的顺序接客,并判断是否可以捎带待接队列的乘客。
也就是说,调度器会在没有输入或电梯待接队列均满时wait,在输入或电梯状态改变时被唤醒。分配乘客时,按照各电梯与乘客是否同向、距离多远、容量剩余等条件进行优先级排序,优先分配给最优的电梯。而电梯则只需要最基础的运行逻辑,一个一个去接待接自己的待接队列中的乘客即可。

即:

输入乘客
分配乘客
分配完毕或电梯均忙
输入/电梯notify
队列非空
队列为空
调度器notify
输入线程
调度器
电梯
调度器wait
接送乘客
电梯wait

2.第二次作业

第二次作业中,我首先迭代了第一次作业,然而效果不理想,于是舍弃了调度器,并重新设计了电梯线程。(调度祭天,法力无边)

电梯线程的运行逻辑在更改之后也并不复杂,即:

公共乘客队列无人
公共乘客队列有人
电梯
电梯内部有人
送最先到达的客人且中途捎带
电梯内部无人
随机上下移动
选取最近的乘客接客

其中的随机上下移动为,一定概率sleep(200),一定概率随机上下移动,主要向6、7层靠拢,于是电梯便不再需要wait就可以避免轮询,且由于电梯大概率集中在中间层,小概率分布在顶、底层,可以更好地应对极端输入情况,运行速度高、等待时间短,与之相对的是耗电量大。

对于新增电梯的指令,只需要创建对应的线程即可,不再赘述。

对于维护电梯的指令,只需要就近停靠,将所有乘客的始发地更改,并重新加入乘客队列即可,也不再赘述。


3.第三次作业

第三次作业相比第二次作业差距不大。首先使用左移运算、与运算实现可达性判断,即:

public boolean reachable() {
    return (mask & (1 << (pos - 1))) != 0;
}

对于换乘,在方法类Factory中新增getRoute方法,来将乘客的路线依据电梯划分为多段,电梯在接客时只需要判断起始、终点层是否均可达即可。例如:
乘客1要从3层到8层,现有两部电梯,一部可以在1-6层运行,一部可以在6-11层运行。

我们首先通过Dijkstra算法,算得乘客最优路径为3-6-8。于是我们将数组{3,6,8}作为其路径,取出3作为起始点,6作为终点。电梯1判断两点均可达而前来接客,电梯2判断起点不可达而不会前来。之后乘客到达6层,又将6作为起点,将8取出作为终点,电梯2此时判断可以接客,因而前来,电梯1判断终点不可达而不会前来。

如果出现电梯维护,则需要对放出的乘客的路线重新规划。同时,为了保证其他乘客的将来路线不包括该电梯,也需要在其他乘客出电梯后重新规划路线。


三、Bug分析与Hack策略

1.bug分析

在第一次作业中,我疏于测试,没有考虑到调度器一次分配不完的情况,因而导致电梯线程过早结束、调度器有乘客而分配不出去,于是卡死的情况。最终只通过了四个强测测试点,没有进入互测。而事后修复时,仅仅改动6行代码即可,让我感到非常遗憾。

在第二次、第三次作业中,我痛定思痛,与室友一起编写了多进程评测机来进行测试,及时在课下发现了一些bug并进行修复。最终两次作业均通过了所有的测试点。不过由于课程组对于性能分的描述错误,导致我误认为电量消耗只占性能分的40%(实际上,是三者计算出性能值,再根据性能值赋分,因而耗电量可以将性能分拉至0),因而性能得分并不理想,最终强测分均只有88。

在debug时,主要采用“输出法”进行测试,即将各个线程的行动具体输出出来,并对比正确行动,以此发现问题所在。而由于多线程具有随机性,未必能够复现,于是我设计了多开测试,方便对同一组数据进行高强度重复测试,来让问题复现。


2.hack策略

关于互测,我第一次作业并没有进入互测。第二次作业中,使用评测机进行七开测试,发现了同房的一个bug。第三次作业中,由于互测限制太多,根本无法对换乘进行测试,在尝试了一个可能的数据(对于开门信号量进行hack)后,没有hack到,便没有再进行尝试。

说到这里,我就不得不说一下互测限制了,这几次作业的互测限制均太过严格,导致无从下手。
一方面,时间限制为220s,于是便没有可能从调度策略上设计最差情况进行hack。
另一方面,限制初始六台电梯只能维护四台,于是便几乎很难从换乘方面hack,因为许多人都是优先选择基础电梯。甚至有些同学压根没有设计任何换乘策略,仅仅是无视新增电梯指令,都无法进行hack,且可以通过18个强测测试点。


四、心得体会

1.关于多线程

在本单元的作业中,我学到了多线程的许多知识,学会了如何使用锁、信号量等保证线程安全,也学会了线程之间如何更好地交互。

在这个过程中,我认为最麻烦最折磨的就是线程的结束问题。早期设计中,我过早的让电梯的isEnd值设置为true,而导致乘客可能没来得及分配。
在之后的修改中,又出现了调度器过早结束的问题。加入maintain操作后,这个问题更加凸显,如果该指令在最后输入,那么就意味着很可能会有被维护的电梯放出的乘客需要处理,如果调度器在得到null的输入后就结束,那么这些乘客就会被忽略,自然不行。

最后,我为结束设计了一个比较合理的机制,即:任何一个电梯都不会擅自结束,而是所有请求处理完毕后,大家一起结束。这样的好处便是不会因为线程结束过早而导致输入处理不完全或者电梯“不帮忙”。


2.关于测试

在本单元的作业中,课程组给出的中测测试点非常弱,以至于我在第一次作业中没有发现重要的bug,而导致强测非常不理想。这也对我们自己的测试提出了新的要求。

我注意到有同学在讨论区分享了自己的评测机,但是我认为拿别人的总不如自己研发的。于是在完成第二次作业后,我与室友一起完成了一版评测机

我负责的部分为数据生成和模块链接。在生成数据时,使用随机数保证生成数据的策略具有多样性,包括乘客请求分布的多样性(上行多、下行多、均匀请求三类)、指令输入多样性(密集输入、分散输入、混合输入三类)、指令策略多样性(新增与维护多、乘客多、无全能电梯等多类)。

同时,在完成模块之间的链接的过程中,我考虑到该单元的CPU使用率较低,因而可以开多个评测进程,来提高评测效率,于是设计了多进程评测机。

最终,我们的评测机也取得了不错的效果,大约可以在一晚上跑八万个左右的数据点,发现了自己的一些bug,并在强测中通过了所有的测试点。

在之后的作业中,我也会继续延续这种思想,多做测试,多建评测机,来提升自己的测试能力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值