oo第二单元——多线程魔鬼电梯

在初步认识了面向对象思想后,立刻进入了多线程的学习,本单元的难点主要是锁的理解,需要保证线程安全的同时防止死锁的发生,也要尽可能缩小锁的范围,提高性能。这一单元以电梯为载体,让我们从生活出发,从电梯运行的角度理解多线程,同时学习和应用生产者-消费者模式来帮助我们编程。在一部可捎带电梯的基础上越来越贴近生活,进行了电梯数量的扩展,载客人数的限制

homework5

这次作业是单部可捎带电梯,主要是初步应用多线程编程,保证线程安全,防止死锁的发生。

UML

采用生产者-消费者模型,Main类创建其他类,启动线程;CubbyHole类是托盘,In类是生产者线程,Elevator类是消费者线程,In把乘客信息放到CubbyHole中,Elevator从CubbyHole取出乘客信息。

方法复杂度度量

总体来说方法复杂度较低,少数方法耦合度高。

自己程序的bug

在强测和互测中均未发现bug。在调试过程中出现电梯线程在应该结束的时候因为还没有执行到跳出循环那一步就满足了wait的条件而wait,导致的线程无法结束的问题。

关于互测

考虑到IDEA手动输入的时间不容易控制,所以用自动化测试,但是并不是很完善,只测试了乘客有没有送达,没有测试电梯逻辑的正确性。所以在互测中并没有发现别人的bug。

homework6

这次作业的电梯数量不确定,需要利用循环开启线程,保证每个线程都能被开启,并且都能获得资源。同时电梯有最大载客量,在调度的时候需要考虑。还加入了负数楼层,这时需要注意0层是不存在的,所以我在1到-1和-1到1的时候特判一下。

UML

 

在homework5的结构上进行功能扩展,仍然采用生产者-消费者模式。在CubbyHole上新增从生产者线程获得电梯数量的方法,为电梯类新增最大载客数和当前载客数的变量。在性能方面没有做太多的优化,只是避免了多个电梯抢一个乘客这种情况。

方法复杂度度量

总体方法的复杂度较低,电梯类的run方法的复杂度较高,可能还是有一定的面向过程的编程思维没有改过来,一些比较细节的东西还是放在了run这个顶层的方法中。

自己程序的bug

在强测中没有出现bug,在互测中出现了一个runtime_error的bug。考虑到不希望进程频繁被唤醒占用cpu资源,所以只在一些我认为需要唤醒的方法中加入唤醒操作,但是并没有考虑周全,在一些应该有唤醒的方法中没有唤醒操作,导致进程无法唤醒,也就无法结束。

关于互测

这次互测我采用了手动测试与自动测试相结合的方式。手动测试主要针对电梯最大载客量的限制,同时在同一层放入很多乘客看是否有超载的发生,还针对所有的电梯线程是否都启动并且都能获得资源进行测试,如果没有,可能会存在RTLE的发生。

homework7

这次作业可以新增电梯,也就是需要在接收到命令的时候再次开启一个线程,还设置了不同类型的电梯,不同的电梯最大载客量不同,可以停靠的层数也不同,所以在电梯类构造的时候就根据不同的类型将这些参数设置好。由于电梯在一些楼层不停靠,这就涉及到换乘问题,主要采用了打表的形式,舍弃换乘两次的情况,只会换乘一次。

UML

仍然采用经典的生产者-消费者模型。不足是存取数据和调度都放在“托盘”里执行,所以托盘类非常的复杂,耦合度较高,不利于扩展。

方法复杂度度量

 

总体方法复杂度不高,大体按照不同的功能写了不同的方法,但是可以看到电梯线程的run方法复杂度较高,这是不足的。run方法应该是最顶层的结构,应该注意层次化设计。

自己程序的bug

在强测和互测中均未发现bug。分析自己的程序,因为调度比较弱,所以可能在指令比较刁钻的时候面临超时的风险。在调试过程出现的错误有忘记换乘的需要而让电梯提前结束。

关于互测

同样,由于在IDEA手工输入对时间的控制较难,所以使用自动化测试,检查了有无丢失乘客,并且可以输出程序的最后一条输出的输出时间,检查RTLE类型的错误。

拓展性分析

功能设计

使用生产者-消费者模型,托盘类负责存取乘客和给电梯分配乘客。电梯类按照电梯运行的逻辑顺序,主要有获取主请求、下乘客、上乘客、移动的功能,电梯的很多参数,如处于的楼层,去往的楼层,电梯内乘客数和乘客信息等都是显性表示的,方法的功能也比较的独立,所以如果要新添加功能会比较方便。

性能设计

考虑到正确性,在性能方面没有做太多的优化,所以后期可以参考一些电梯调度算法,如look,sstf从单部电梯的角度来优化性能;也可以参考图的最短路径的一些算法,对换乘站的计算等方面来优化性能;也可以考虑如果有多部相同类型的电梯,让它们只在较小的区域活动等优化方法。

三次作业的设计策略

三次作业都采用生产者-消费者的模式,建立主类,生产者类,托盘类和消费者类,生产者线程和消费者线程只会获取托盘的数据和方法,做好托盘的保护,两个线程不会互相调用。在多个电梯的作业中,并没有为每个电梯设置等待队列,而是采用电梯到达一层时将总的等待队列的符合要求的乘客都接走这种方式。乘客的分配和电梯调度都由托盘类根据等待队列的乘客状态做出判断。这样做有利有弊,好处是只有一个共享资源,所以不会线程互相等待对方的资源而出现死锁的情况。坏处是这个托盘类非常的复杂,方法、变量都特别多,耦合度也很高,不符合类的要求,可扩展性也比较差。

在设计中满足SOLID设计原则

SRP原则

按照生产者、消费者、托盘划分类,功能明确,但是由于题目比较复杂,在迭代过程中因为并没有拓展更多的类,所以类非常复杂,这个需要反思与改进。

OCP原则

把电梯按照逻辑划分为寻找主类、下乘客、上乘客、移动这四个主要的方法,在迭代时原有功能没有太多改动。

LSP原则

本次作业并没有过多涉及。

DIP原则

本次作业并没有过多涉及。

心得体会

多线程编程与单线程编程的思维相差比较大,调试的难度,出错的概率也很大。初次接触多线程的时候,我觉得弄清楚“锁”这个概念很重要,公共资源就像是一个房间,每次只能一个线程拿到锁进入房间,没有拿到锁但是又要进入房间的就需要等待,执行完synchronized块后自动归还锁,wait是交出锁并且到等待区,并不是等待锁的队列,notify和notifyAll是从等待区唤醒线程进入等待队列。我觉得生产者-消费者模型对我们写多线程程序很有帮助,这次作业非常符合这个模型,所以应用这个模型可以让我非常快的上手这次的作业,并且对于三次作业即使功能上有了很多的扩展,但是这个模型都非常适用,所以相比于第一单元的疯狂重构,这个单元没有出现大的重构。

我觉得最难的部分是调试,单线程用起来非常香的断点调试在多线程也就不香了。我觉得想要调试多线程,首先是先要把整个运行的逻辑梳理一遍,什么条件电梯不应该运行而应该睡眠,什么条件线程应该结束,特别是唤醒操作,有些synchronized方法块中必须要有唤醒操作,不然可能会导致线程不能被唤醒从而无法结束,有些方法块中加入唤醒是不必要的,导致线程不应该唤醒的时候唤醒,占用资源。我主要使用printf来调试,比如遇到cpu时间特别长的情况,这时在while循环中设置自增的变量,每次循环就加1并输出,有很多次我的程序都出现了这个数值特别大,原因是条件设置不合理所以应该wait的情况下没有wait导致。

猜你喜欢

转载自www.cnblogs.com/lxy-oo/p/12716859.html