第二单元博客总结
目录:
1. 多线程协同和同步控制策略
2. 第三次作业分析
3. 基于度量分析
4. bug分析
5. 心得体会
一、多线程协同和同步控制
Homework1
上面是第一次作业设计的程序架构。可以看到,除了主线程以外,又开启了三个额外的线程,分别是Controller,IElevator和Exchanger。
主线程负责的是初始化其他线程,并接受输入。
Controller则是负责利用一个合适的算法计算出电梯的停靠楼层。
IElevator是一个非常简单的电梯模拟器,它仅仅完成一些电梯的动作,比如移动、开门关门之类的动作。
至于让人员进出电梯,则是由Exchanger来完成。
三次作业从架构上讲基本没有很大的变化,所以我就以第一次作业为例,介绍一下我的多线程协同策略。
我们在进行多线程的编程的时候,遇到的最常见的问题是数据冲突和死锁。
对于数据冲突的问题,我们先锁定会发生数据冲突的地方,也就是公共数据。在我的框架中,只有在Warehouse和RequestPond中。对于Warehouse来说是很简单的,因为Controller和MainClass仅仅通过Warehouse构成生产者消费者关系,所以在Warehouse中只需要用Synchronize锁住就可以避免数据冲突。
比较复杂的是在RequestPond中。在直接涉及到数据冲突的地方,比如说Controller会写RequestPond,elevator会读RequestPond,这样的情况是比较容易解决的,我们只要用Synchronize关键字对访问进行排队就可以了。另一个问题是,Controller会把RequestPond中的信息取出来,在线程中进行运算,将结果再写回到RequestPond中。在运算的时候,controller并没有持有RequestPond中任何属性的锁,所以就无法阻止elevator和Exchanger对RequestPond进行访问和修改。为了解决这个问题,我的解决方法是设置一些标识位,使得如果controller在本地计算的时候,如果其他线程想要对RequestPond进行修改,就会先进行wait(),然后等待controller来唤醒他们。
其次是死锁问题。死锁发生的一个必然要求是一个线程想要同时持有两个变量的锁。在第一次作业中,并不会出现这样的问题,因为所有线程都只会持有一个变量的锁。
Homework2
在第二次作业中,相比于第一次作业有两处修改。一个是消除了Controller对IElevator的依赖。在第一次作业中,这个依赖主要是用于Controller获取elevator所在的楼层。在第二次作业中,楼层的获取交给了RequestPond。经过这样的修改之后,elevator和controller就不会有直接的联系了,而是完全通过RequestPond交换信息。所以在之后controller管理的就是以RequestPond为代表的RequestPond、elevator、Exchanger结合体。所以每要多管理一个电梯,我就增加一个这样的结合体在Controller中。
另一个修改是Controller现在要管理多个RequestPond,所以在设置标志位、更新RequestPond和调度算法上做了相应的修改。
Homework3
在第三次作业中我做的主要修改是,增加了RequestPond对于Warehouse的依赖、MainClass对Controller的依赖和一个新的类Person。现在我们在交换信息时不是交换Request了,而是交换Person。这样做的原因是,在这种情况下,我们就可以保存更多的信息,也可以对Request所保存的信息进行一定的修改,使其符合当前情况。
RequestPond对于Warehouse的依赖是为了在有人需要转电梯时,可以把Person放回Warehouse中,让Controller重新获取,进行路径的规划。
增加了MainClass对Controller的依赖是为了添加Controller中管理的电梯。
二、第三次作业分析
(一)功能设计与性能设计的平衡
再分析一下第三次作业的功能设计和性能设计的平衡。事实上,在我的设计中,由于elevator没有计算能力,Controller承担的压力太大了。
Controller作为中央调度器,为了实现调度,事实上是对现实的电梯运行进行了一个更为快速的模拟,获取到每个电梯需要停靠的楼层和所要交换的人。所以controller最后就变的十分复杂,这可能也与我的代码书写不太简洁有关。
另一个就是,为了避免电梯间的交互,我会将一个需要转机的需求拆分成两部分。在执行完前一部分时,我是无法对第二部分进行一个规划的,所以也就无法很好的将第二部分的执行纳入到规划中。
所以在功能上可能并没有什么问题,但是如果要追求性能,就会导致Controller中的代码非常多,看起来非常臃肿,并且也很难得到一个很好的调度策略。
(二)solid原则
-
单一责任原则
从类的方面来讲,我想我还是比较好的遵守的单一职责原则。各个类都负责一个单一的工作,比如controller就负责调度,elevator就负责计时,exchanger就负责交换人员等。不过还可以有一定的提升,比如说RequestPond可以进行进一步的拆分,使得controller、elevator、exchanger两两之间共享的资源更少。
-
开放封闭原则
对于开闭原则我并没有太好的一个应用。每次如果要修改需求,我都会更改掉一些代码来满足需求。所以这一点可能做的不是很好。
-
里氏替换原则
对于里氏替换原则,这次我并没有利用继承,所以也没有很好的应用。
-
接口隔离原则
对于接口隔离原则,就如在单一职责原则中说的,可以对RequestPond进行开分,这样就可以比较好的实现接口隔离原则。
-
依赖倒置原则
对于依赖倒置原则,我想我利用Person对PersonRequest进行封装应该也算是一种依赖倒置,使得其他模块依赖Person而不是PersonRequest。当然课程组对于PersonRequest和ElevatorRequest的封装更符合依赖倒置原则。
三、基于度量分析
Homework1
Homework3
我的最高的圈复杂度是出现在第三次作业的Person类中,在这个getTransferFloor的作用是让我们能获取到转电梯的楼层,所以我用了switch case的方法对不同的转机情况进行了处理。感觉这里比较好的一个处理方式其实是讨论区里的说的通过floyd算法进行计算并打表。方法长度更短,效率也要更高一些。
四、bug分析
(一)自身bug分析
我的程序在强测里面并没有出现bug,所以我就就中测中出现的两个bug进行一下分析。
第一个bug是第二次作业中出现cpu TLE,这个bug出现的原因是我的exchanger类在完成交换后没有将自己关闭,而是一直在一个死循环内运行,造成了cpu时间暴增。
第二个bug是第三次作业中出现的Person数据莫名其妙消失的问题。这个原因是由于我添加了MainClass对于Controller的依赖,并且将添加电梯的工作直接交给了MainClass执行,最致命的是,我在添加完电梯后会直接进行一次电梯调度的更新。这就是的电梯调度的更新可能由两个类来进行,但是保存在controller中的新Person在进行一次计算后就会被清零,所以就导致后执行完的调度更新不能得到新来的Person,但却对电梯调度起了决定性作用。
对于第二个bug,我的解决方式让MainClass只是将需求交给controller,将增加电梯的工作交给controller自己完成,并且不会在一次更新中计算两次调度即可。
(二)bug发现
在第一次作业中我没有使用自动测评,打算自己阅读代码寻找bug,但这样效果并不理想。所以在第二次作业中便加入了自动测评工具的使用,主要是检测正确性,对于cpu时间并没有检测。
五、心得体会
在这次作业中,感触最深有两点,一个是迭代式开发,另一个是复杂问题的分析。
先说迭代式开发,其实在第一次作业中,我并没有感觉出迭代式开发所带来的一些好处,比较重要的一个原因是是我当时并没有这样一种意识,说要自己的代码要有一个良好的可扩展性,所以就导致了每次都要做比较多的修改。但是这次作业中,我从第一次作业就开始进行了设计,虽然不是很完美,但是也算是勉勉强强的用这个框架完成了整个单元的作业,这样为我节省了不少时间和精力。
另一个比较有感触的是对复杂问题的分析。这个单元的三次作业对于电梯的调度的限制越来越多,使得问题越来越复杂。所以在写调度算法的时候想要去考虑的分支就变得越来越多,对于很多种情况都会有一个个例子迫使你去写一个个分支去处理,否则就不能得到一个最优的调度。我认为遇到这样一个问题的原因,一个是我对问题的抽象还不够,没有找到一个相对统一的解决问题的方法,另一个是问题的复杂性不太允许我们得到一个完美的结果。对于电梯问题的分析问题我到最后也没能很好的解决,最终只能拿出一个不是很好的但很统一的调度算法来使用。