第五次作业 单部多线程傻瓜调度(FAFS)电梯
设计构架:
这次作业完全是生产者——消费者模型的翻版。所以设计起来可以说是照搬照抄。我设计了Input类和Elevator类分别作为生产者和消费者,用Controllor(这是个拼写错误)作为共享对象。Input类每次读入请求,交给Controllor,放入一个线程安全的队列中。Elevator类在一个while循环中不断去Controllor中的队列中读取请求(如果读取不到就sleep50ms)。对于ctrl+D的结束输入呢?我采用了这样的思路:Input类可以检测读入是不是null,如果读入null,就将共享对象中的一个flag置为true并且自身break,每次电梯去访问共享对象中的请求时候,都会检测这个flag,如果flag==true,就直接break。
UML分析:
这次作业的类之间关系是三次作业中最清晰的,因为完全是照搬生产者—消费者模型。Test类为主类,开启Input和Elevator两个线程。
Controllor的getItem和queueOut分别是得到和拿出请求的方法。Elevator中zero到six共7个方法描述了电梯从一层运行到另一层以及开关门进出人的全过程。为了防止run方法超过60行写的
对于方法的复杂度分析:
复杂度比较低(相比于之后两次作业)
但是这次作业在开放封闭原则方面做得很有欠缺。导致了第二次作业代码的重构。
我出现的bug:
这次作业没有在强测和互测中出现bug。本地测试也没能发现bug。
怎么找别人的bug?
使用了许多压力测试:
1.同一时间点大量输入从1到5楼的指令
2.同一时间点大量输入夹杂着错误指令的正确指令
可惜都没有找到别人的bug
心得体会:
线程安全是啥?怎么实现线程安全?在完成这次作业之前我一脸懵逼。但是这次作业中我明白了一些。
线程安全是啥?
线程安全就是通过同步机制保证各个线程都可以正确的执行,
怎么实现线程安全?
1.所有的方法前面加synchronized(除了run)。虽然有很多确实不用加synchronized,但是为了编程简单和不出错误,又何乐而不为呢?
2.使用线程安全的容器,例如这次作业中储存请求的队列使用线程安全的LinkedBlockingQueue。虽然理论上使用了第1点后并不需要使用线程安全的数据结构,但是又何乐而不为呢?
总之,多一重保障,多一点放心。
设计模式方面,照搬了生产者-消费者模型之后,似乎没有必要讨论设计模式,抄就行了
第六次作业——单部多线程可捎带调度(ALS)电梯
设计构架:
第六次作业=第五次作业+实现捎带功能。所以本次作业的设计的一开始,我想和上次一样,使用Input类读入输入,放入Controllor类,并由Controllor类实现对电梯Elevator类的调度。而Elevator本身不具备任何“思考”的能力,即只实现开门、关门、移动等“机械”的能力。但是随后实现中(代码写完了之后)我发现一个重要的问题:Controllor类由于需要获知电梯在哪里、处于开关门状态等大量电梯的“机械”属性,导致Controllor和Elevator之间的耦合度极其之高。思索再三之后,为了实现“低耦合”的设计思想,我直接重构了代码。
重构后的构架仍然使用Input类读入输入,放入Controllor类中的一个队列中。Controllor只有3个主要的方法,put(放入请求),getMain(取得主请求),getSub(取得捎带请求)。而交由Elevator类来实现“是否捎带”、“如何捎带”等问题。这样的构架虽然和日常生活相违背(生活中是调度器控制电梯,而不是电梯控制它自己),但是有效降低了类之间的耦合度。所以仍然是一种可行的设计。
UML分析:
对于方法的复杂度分析:
对于类的复杂度分析:
虽然WMC算不上很低,但是至少在直观上,电梯和调度器放在一个类中,确实可以降低耦合。
但是我觉得用了合适的设计模式,可能可以没有那么高复杂度
这次作业在单一责任原则上做得有欠缺,Elevator类承担了电梯运行的“机械”细节和运行调度两个主要职责,这是不合理的,但是也是设计中无奈的妥协。
我出现的bug:
这次作业在本地没有找到bug,没有在强测和互测中也没有被找到bug。
怎么找别人的bug?
从来不分析别人的代码,都是盲刀
使用了以下几种边界测试用例:
1.在一个时间点投入很多个从-3楼到16楼的请求。
2.在一个请求输入0.2/0.4秒后输入多个可以捎带的请求,试图让别人的电梯不能完成捎带出现bug。
3.在多个时间点随机投入样例,测试别人的电梯是否能再多个时间点实现输入的正确处理。
可惜并没有找到别人的bug。。。。。。
心得体会:
在线程安全方面没什么好说的,和上次作业一样,方法前加上synchronied。
在设计模式方面,确实很有缺陷。由于老师在课上对于设计模式都是点到即止,我在课下进行代码构架设计的时候根本不愿意去用那些模式(因为在确定用我自己设计的构架能够完成设计指标的时候,又有谁愿意去使用一个不熟悉、第一次使用、又没有直接好处的设计模式呢?)但是我也很希望能学到一些设计模式,所以还是希望老师能在课堂上多讲授一些关于设计模式的知识(尤其是应用场景),而不是点到即止。
第七次作业:多部多线程智能(SS)调度电梯
设计构架:
这次作业可以说是前7次作业中最困难的一次。困难在于两个方面:
1.多线程程序对于调试很不友好(bug不可复现、无法使用IDEA调试模式)
2.需求本身十分复杂。这次指导书中对于不同电梯的停靠楼层、容量、速度都和实际场景有很大差异,可以说就是为了编程而设计的场景。但是话不多说,该干还得干。
我采用了两级流水调度器的结构。第一级为Controllor,分配请求给各个电梯。第二级为Elevator,调配电梯内的请求执行顺序。首先为了方便对于请求的处理,我写了一个Request类来代替PersonRequest类,主要区别在于多了一个变量来标记请求是否可以被执行(有的请求因为乘客还没到达fromFloor而无法被之执行)。又由于上次作业代码构架良好,本次作业大量继承了上次作业的代码,实现了良好的复用。Input类和Elevator类几乎没有变化,Controllor类中提供的主要方法依然是put(),getMain(),getSub(),功能分别为得到请求,取出主请求和取出附属请求。但put()方法中对于那些不能直接到达的请求做了拆分,统一安排到1楼或15楼进行换乘,拆分为两次可以直接到达的请求。有一个值得注意的细节是:对于换乘中的第二步,需要标记它暂时不能进行,知道第一步换乘完成后,才能标记它可以进行。getSub()方法中还需要增加对于电梯内人数的检测。
尽管我的初心是尽力减少Controllor和Elevator之间的耦合,但是最后还是需要很多互相通信的方式,从而提升了耦合度。
UML分析:
对于方法的复杂度分析:
对于类的复杂度分析:
可以看出,这次作业中类的复杂度太高了。尤其是Controllor和Elevator进行交互的几个方法,都十分繁杂(因为充满了各种检测的信号量)。但是说实话,我的设计开始时虽然尽量规避这种情况,但是在写代码时候仍然还是需要各类信号量来方便检测其他类的状态。这也是为什么我想使用设计模式的一个原因。因为这样复杂的代码在调试的时候也确实是一场噩梦。
这次作业在单一职责原则上依然不尽如人意,Controllor类承担了对于请求的读入,排序,取出,设置标记位等许多职责和Elevator类也承担了电梯运行细节和运行调度两项职责。
我出现的bug:
这次强测出现了一个bug,互测没有被测出bug。强测的bug是输入指令导致死锁:多个进程都进入睡眠,最后导致时间超过210秒。
这个bug的解决方案:在main方法中添加这么一段,确保程序在200s时可以退出。
try {
Thread.sleep(200000);
} catch (Exception e) {
System.out.println("error");
}
System.exit(0);
怎么找别人的bug?
这次有大佬给我提供了评测机,就开评测机产生的随机数据,都在同一时刻点投放,进行压力测试,就可以全自动刀人啦~
最后提交了7个测试用例,找到别人30个bug。
(既然有测评机,也就没法讨论啥找别人bug的方法论了)
心得体会:
这次的程序构架复杂,在线程安全方面我犯了死锁的错误。也就是三个电梯线程全部sleep,导致整个系统call down。这也是我在本地评测机使用了上百个样例依然没有发现的情况,但是就是在强测中出现了。。。说明对于多线程程序的测试是永无止境的(bug千千万)。但是死锁bug即使发现也很难调节(因为没有调试器的帮助,而且40条请求让人看得眼花缭乱),所以更需要在设计的时候就注意到死锁的问题。
在设计模式方面,这次仍然没有采用任何的设计模式。但是我觉得自己这次作业构架混乱,也是最后强测出现一个bug的原因之一。所以更加渴望能在之后的作业中采用已有的设计模式,让自己的程序更加逻辑清晰,条理明确,从而方便调试。
这三次作业,在短短20天内,让我从对于多线程编程从一无所知,到略懂皮毛。收获确实很大。
而且我发现IDEA的STDIN/STDOUT窗口在输入时会小概率未能读入的bug,所以对于多线程的程序,用管道输入是一个更可靠的选择。
继续努力吧!