BUAAOO第二单元——多线程任务

一时周伯通与裘千仞斗,一时郭靖与裘千仞斗,一时欧阳锋与裘千仞斗,一时周伯通与欧阳锋斗,一时郭靖又和周伯通交手数招。四人这一场混战,就中周伯通最是兴高采烈,觉得生平大小数千战,好玩莫逾于此。

一、代码静态分析

1、第一次作业

类关系

类属性/方法

线程关系

复杂度

扫描二维码关注公众号,回复: 5982430 查看本文章

可以看到,第一次作业结构相对简单。线程设计中main并未充分利用,只是用来开其他线程。方法复杂度均不高,只是在两个线程的run方法中由于涉及一些判断,复杂度略有升高。

2、第二次作业

类关系

类属性/方法

线程关系

复杂度

第二次作业中我将main用作输入进程,可以节约一个子进程。出现了两个复杂度较高的方法interact和dispatch,这两个方法都用于到达楼层时电梯和调度器之间的交互,由于本次作业需要捎带,因此判断逻辑更为复杂,增加了复杂度。

3、第三次作业

类关系

类方法

线程关系

复杂度

第三次作业线程数目增多,但由于三个电梯是基本相同的,并未带来更复杂的线程关系。复杂度较高的方法中,getInFlag、hasLegalOrder涉及对容器的遍历和判断,Dispatcher、split是根据给定的电梯规格进行了性能好但不优美的粗暴逻辑处理,run和调度器交互时有大量的判断逻辑。

4、基于SOLID的分析

Single Responsibility Principle:这方面做得不错。设计了很多功能单一的轻量级方法,在主要方法中调用这些轻量方法,而不是直接写复杂的功能逻辑。

Open Close Principle:电梯的设计很好的做到了这一点,所有参数都经外部设置,更改需求无需修改电梯内部实现。调度器并未依照此原则,主要是担心对一般情况的讨论会导致大量遍历和判断、性能极差,最终采用了“魔法”化的处理。

Liskov Substitution Principle:这三次作业几乎没有出现继承。

Interface Segregation Principle:这三次作业中几乎没有出现接口。

Dependency Inversion Principle:这一部分其实官方的输入输出接口已经帮我们实现了。所有的IO目标都是可更改的流,并不绑定于标准输入输出。

二、设计策略

1、第一次作业

第一次作业在多线程控制方面只需要注意两点:要保证请求队列线程安全;电梯线程需要知道输入是否关闭。

我将电梯的run设计成一个while循环,循环条件是(输入未关闭 || 请求队列非空),循环体是电梯的一次运行。队列线程安全通过给队列的进出操作加锁实现;维护一个表明输入开关状态的变量,电梯线程通过检查该变量控制线程结束。为了不过度轮询,在发现输入未关闭且队列为空时,sleep一小段时间。

2、第二次作业

在电梯线程的控制方面,我放弃了sleep+轮询,改用wait+notify。电梯检查到不应退出线程但当前没有可满足请求时wait,main添加新的输入时notify唤醒电梯线程。需要注意的一个小坑点是,如果输入关闭时电梯正在wait,它就将一睡不醒……所以关闭输入后也要notify唤醒电梯线程,目的是让它检查更新后的循环条件,以便退出。

3、第三次作业

依然采用wait+notify,但由于电梯线程是三个,使用notifyAll更加合适。由于我在处理分段请求时采用从请求先吞后吐的方式,因此即使输入以关闭且请求队列为空,也不意味着电梯线程可以结束,因为将来可能有其他电梯线程再向请求队列中添加请求。这样一来循环控制条件变得更为复杂,尤其需要注意循环控制条件和wait条件之间的异同,处理不好极有可能出现轮询(血的教训)。

 三、BUG分析

本单元任务并不像上个单元细节问题那么多,多线程的测试和bug复现也是个问题,再加上三次都被分到了儒雅随和ROOM,最终本单元战绩hack0/hacked0……所以本章主要分析我在中测中出现的bug。

第一次作业中,我在线程结束条件处出现了问题,也就是群友常说的“提前下班”。这个bug被很快修复,同时我意识到,线程间通讯可能是本单元最为棘手的问题。

第二次作业又死在了线程结束条件。首次采用wait+notify,结果出现了睡死现象。最初的设计是在新增请求时notify电梯,那么若电梯wait时输入关闭,电梯将再也不会收到notify,一睡不醒。解决方法是在关闭输入时也进行一次notify,通知电梯更新循环条件。

第三次作业在循环/wait条件处疯狂翻车,先后出现了无法安全退出、退出过早、轮询的问题,其主要原因都是循环和wait的条件没有设计好。重点说一下轮询的问题,我的电梯run流程是这样的:

run() {
    while (!isListEmpty || hasFutureRequest) {
        while (!hasProperRequest)
            wait();
        dispatch();
        work();
    }   
}

由于isListEmpty、hasProperRequest以及dispatch内部的工作逻辑各不相同,如果设计不好,就会出现这种情况:满足第一个while条件,但不满足第二个while条件,进入dispatch但没有收到任务,于是work直接退出,回到第一个while,重复。所以在设计时必须考虑周全,保证在所有情况下都不会出现空跑的现象。

四、心得体会

1、调度策略

前两次作业均依据指导书思路,实现较为简单。第三次作业采用了LOOK算法,虽然未进行深度优化,但性能大体能够令人满意。

2、拆分策略

第三次作业涉及对请求的拆分。我的实现思路是,封装一个SplitRequest类。SR类内部封装一个PersonRequest列表,存储被拆分后的请求。SR类只有第一个请求能够通过currentRequest方法对电梯可见,电梯在满足CR之后,调用SR的toNest方法将CR更新为下一个PR,然后将SR送回请求队列。这样一来对于每部电梯,收到的请求依然是可直达的原子请求,不必大改电梯运行逻辑。

3、线程安全

线程安全是个大问题,而且一旦出现bug难以复现和调试,必须在设计阶段将隐患全部消除。全部加锁固然保证安全,但是会造成性能的极度下降,甚至陷入死锁。要做到合适的同步,要对共享对象和可能引起不安全的过程了然于胸。(其实我也不清楚自己最终是否有安全问题……)

4、设计原则

尽量避免硬编码,程序和数据应该分离。我的设计中只出现了一次硬编码,我认为在一般条件下拆分请求涉及的逻辑过于复杂,于是直接采用了硬编码。

多写功能单一的小方法,避免出现一两百行复杂逻辑的大方法。

要支持拓展。这里想到一个细节问题,如何给电梯注册调度器?我最初是直接在电梯初始化时调用调度器的单例,但后来发现将调度器对象作为参数传入更好。因为电梯作为一个可复用类,应该与调度器分离,即电梯不需要知道调度器对象是如何取得的,单例获取应该在main中实现。

猜你喜欢

转载自www.cnblogs.com/AbyssGazeaAlso/p/10753094.html