BUAA_OO_第二单元作业总结

第二单元作业总结

BUAA OO 2021面向对象作业

19373469 陈纪源

一、同步块和锁的选择

(一)第一次作业

​ 在本次作业中,由于是刚刚了解的多线程的内容,因此参考了实验课的框架。使用syncronized语句对于共享数据waitQueue(等待队列)和processQueue(局部队列)所包含的代码段进行互斥共享。下面分别介绍各个线程的同步控制块。

​ 输入线程:

while (true) {
	PersonRequest request = elevatorInput.nextPersonRequest();
	if (request == null) {
        synchronized (waitQueue) {
            //close
        }
        break;
    } else {
        synchronized (waitQueue) {
            //add person
        }
    }
}

​ 调度器线程:

while (true) {
	synchronized (waitQueue) {
        if (/*judge something*/) {
            for (int i = 1; i <= elevatorNum; ++i) {
                synchronized (elevatorQueues[i]) {
                    //elevatorQueues[i].dosomething
                }
            }
        }
        /*do something else*/
	}
}

​ 电梯线程:考虑到实验课架构是有死锁的可能性,我在这里修改了一下,没有再抓取waitQueue的锁

while (true) {
	synchronized (elevatorQueue) {
        try {
            elevatorQueue.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (/*judge something*/) {
            //work
        }
        move();
        elevatorQueue.notifyAll();
    }
}

(二)第二次作业

​ 第二次作业虽然加入了多部电梯,但是由于我在上一次作业中写好了调度器,因此在架构上没有太大区别(依然使用了同步控制块)。唯一有区别的是我发现我上一次的架构,电梯和调度器实际上是串行的。

​ 原因在于,我的电梯线程每动一层,都会wait()等待调度器线程通过notifyAll()唤醒电梯线程,同时move函数中电梯线程并没有释放出elevatorQueue的锁,因此电梯只能白白等待。

​ 因此我对电梯线程的锁进行了修改:

while (true) {
	synchronized (elevatorQueue) {
    	if (/*judge something*/) {
        	//work
        }
        elevatorQueue.notifyAll();
    }
    move();
}

​ 这样调度器和所有电梯线程就可以做到高并发了。

​ 同时这次由于是有多电梯,因此输出需要加锁,我写了一个Output类来处理输出,当需要输出时会调用其中的互斥方法printString()

public class Output {
	synchronized void printString(String str) {
    	TimableOutput.println(str);
    }
}

(三)第三次作业

​ 第三次作业中由于需要换乘,因此增加了一个新的共享数据类OutPerson,表示从电梯出来需要换乘的人的集合,因此调度器线程需要进行修改。

while (true) {
    synchronized (waitQueue) {
    	 if (first) {
             for (int i = 1; i <= 5; ++i) {
             	synchronized (elevatorQueues[i]) {
                 	//elevatorQueues[i].dosomething()
             	}
             }
         }
         else if (waiting) {
             if (exchange.size() > 0) {
             	synchronized (outQueue) {
                    try {
                        outQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        //deal exchange
                    }
                }
             } else (/*judge something*/) {
                 /*do something*/
             }
         }
         else (/*judge something*/) {
             /*do something*/
        }
    }
}

二、调度器设计

(一)第一次作业

​ 本次作业中调度器只起到了传输的作用,把数据请求从waitQueue传输到processQueue,并没有做真正的调度工作。

​ 而且由于自己理解不够深刻,导致调度器和电梯的交互变成了串行模式,性能很低。

(二)第二次作业

​ 这次电梯调度中调度器发挥了分配的作用,分下面两种情况讨论:

  1. 新加一个电梯:将所有processQueue中的人全部取出放回waitQueue,再根据当前电梯情况进行重新分配(跳转到2)。

  2. 新加一堆人:跑模拟电梯,贪心求出将人放入哪个电梯所有电梯的最大结束时间会比较早(如果有多个则将其加入等待人数比较少的电梯中)。

    调度器根据以上策略进行请求分配。

    调度器和输入线程(Main函数)的交互主要是获得输入请求。

    调度器和电梯线程的交互是每次调度会去获取电梯的状态,分析得到新的请求的分配策略。

(三)第三次作业

​ 这次调度器在上一次的基础上,加入了换乘的功能。

​ 类似于第二次作业,对于每个请求的人会获得一个放入电梯的编号。

​ 当电梯是A电梯时,且该电梯的等待人数\(A_w\),B类电梯最大的等待人数\(max B_w\),C类电梯最大的等待人数\(max C_w\)满足:

\[(max B_w + max C_w) \times 2 \leq A_w \]

​ 就认为是不平衡的,此时强制电梯换乘,换乘表是一个静态策略表。

​ 这次调度器和电梯的交互在上一次的基础上,还增加了换乘人员的交互,相当于从电梯线程出来的人,如果它需要继续换乘,那就需要通过outQueue传回waitQueue当做一个新的请求。

三、第三次作业架构分析

1. UML类图

2. UML协作图

​ 该图只画出了最开始前三个电梯对应的UML协作图(后面的电梯加入是类似的)。

3. 功能和性能分析

​ 分析本次电梯的功能:从输入线程获得请求,将请求交给等待队列中。调度器根据等待队列,将请求交给模拟电梯,返回建议放置的位置。当调度器发现电梯内部不够平衡时,强制人员换乘,从List中获得换乘(静态)表,将请求分给对应的电梯等待队列,否则就直接将人的请求给对应的电梯等待队列。电梯线程获取电梯等待队列,使用look算法进行上下移动,当发现一名用户属于换乘用户时,需要加入OutPerson中的队列。OutPerson中的序列会将人重新加入等待队列进行重新分配。

​ 分析本次电梯的性能:模拟电梯在第二单元的作业中取得了一定的效果,但是在没有换乘的情况下,有可能会使所有的乘客都去A电梯(因为对于B/C电梯,有些乘客请求是无法到的),因此采用了之前所说的动态强制换乘操作。但是由于时间仓促,测试并没有太及时,这个换乘的参数选择其实并没有得到特别好的效果。

​ 平衡性分析:为了追求换乘的效果,我在之前第二次作业中的框架下修改了大量东西,增加了大量的共享数据(还是在waitQueue和ElevatorQueue中增加的),这导致我后来调试中有大量的问题。而且这种换乘策略我觉得自己也没有想太清楚,甚至都没有做太多和不换乘比较的策略。因此在功能和性能的平衡性把握中,我还需要再反思反思。

4. 架构可扩展性分析

​ 对于我的架构来说,扩展性非常一般。对于我的电梯线程,并没有使用状态模式建模,还是基于很多的if判断语句(非常面向过程!)。个人认为电梯线程还是应该分解为状态转化类和操作类,状态转化类负责修改电梯的状态(对于不同的模式就可以仅仅修改这个部分),操作类负责电梯的开门、关门、上下移动。调度器线程理论上来说扩展性还可以,但是其实调度器线程其实最好将调度和策略分成两个不同的类,分别管理自己的事情,这样如果有新的调度策略时可以仅仅修改策略类。

5. 反思与总结

  • 将策略和动作分成不同的两个类,而不是柔和在一起

  • 应该考虑将Person类重新封装,以更好的进行换乘

  • 性能优化应该建立在稳定的架构上的,而不是舍本逐末

四、分析自己程序的bug

(一)第一次作业

​ 在第一次作业中,所有公测数据和互测数据都已通过。

(二)第一次作业

​ 在第二次作业中,所有公测数据和互测数据都已通过。

(三)第一次作业

​ 在第三次作业中,最后一个公测数据点出现了bug。

​ 经过分析发现是自己在给新电梯分配可到达楼层数组时错误赋值了。

​ 出现这个bug的一个关键性原因是,调度器类的功能实在是太多了,行数有341行。

​ 这么多的行数情况下非常容易写出这样的bug。

​ 线程安全问题做了很多测试,没有问题。

​ 这提醒我一定要去做类功能的拆分,既方面自己理解,又可以更好的进行代码调试。

五、分析其他同学程序的bug

(一)第一次作业

​ 第一次作业当时就采用了一些随机数据(30条指令)对一部分同学的代码进行测试,结果没有测出问题,就没有在上面提交数据。

(二)第二次作业

​ 第二次作业依然使用了随机数据(50条指令)对一部分同学的代码进行了测试,发现其中一名同学的输出没有进行同步互斥,会出现输出逆流的问题(时间戳并没有按顺序),将数据提交到评测机后发现并没有复现bug(本地基本上每次都有问题)。

(三)第三次作业

​ 第三次作业还是使用了随机数据(50条指令),这次由于一些原因是和我的两位队友一块测试的,对它们的代码进行了测试发现并没有问题,也尝试在上面提交了一次数据,发现没有bug。

六、心得体会与反思

6.1 线程安全

  • 通过第二次作业的磨练,对多线程的内容有了更加深刻的理解。
  • 认识到了多线程的各种架构(任务分配、观察者模式等),认识到各种架构的优势和劣势。
  • 认识到锁和同步控制块的重要性,控制块的大家既不可以过大也不可以过小。
  • 在测试多线程线程安全时需要在高并发的环境中模拟进行,且不可以仅仅单一进程测试,这样效果不好。
  • 认识到线程安全类的重要性。
  • 认识到线程安全问题的调试方法(断点调试和printf输出中间结果),还可以对功能进行拆分测试。
  • 还需要思考如何进行科学的多线程测试,主要是覆盖所有的分支和情况。

6.2 层次化设计

  • 架构设计非常重要!!!这次的架构问题就是在设计时没有进行策略和行为分开,导致可扩展性差。
  • 学会平衡功能性和性能,性能的提高应该是在一个好的功能性基础上进行的。
  • 认识到状态类建模和策略类建模的方式。

6.3 其他

  • 继续提高自己工程开发的能力,也进一步锻炼了自己的代码能力。
  • 对于自动评测机,还需要了解如何扩展和改进。
  • 更需要和同学们、老师们交流架构模型,从其他同学那里学习到新的思路。
  • 对于更多架构和模式,还需要进行更深入的学习。
posted @ 2021-04-26 00:30  生物圈的自传  阅读(68)  评论(0编辑  收藏  举报