一.前言:
上次作业,我没想到我苦思冥想写了好久最后出来只写了这么短,显得很敷衍,但了解我的人都知道,我确实表达能力有限,所以上次就严格按照要求的提纲一条一条的分析了,希望助教大大看到不要怪罪,我真的认真写了Orz。
上次作业不知道是哪位仁兄评论说代码构造分析的很好,很详细,我很感动,不过他也提出来了一个问题,就是一些指标的分析不够详细,因此这次我会额外加上这一部分。我会尽力讲的清晰一些,不过希望有限能读到我的blog的人,能多多包涵。
另外在这么多的大佬和巨佬面前,我非常的惭愧,我写代码的说不上好,但我觉得让更多的人看到一种新的画风,多多少少还是能给人一些感受的,哪怕是有则改之无则加勉的教训,我相信一定能给别人带来新的感受的。
除此以外,我知道三人行必有我师,所以希望看到我的blog的人,能抽出一点时间在底下评论我一句(啊我说的是评论,不是求赞,我并不奢求这个),指出哪里不好,代码、博客都可以(悄悄说,如果能加个好友就更好了),希望能共同交流共同进步。
下面就是,我对我这三次电梯作业的一些看法了。

二.设计策略和基于度量的分析:

1.三次作业总述:
下面是框架:
下面我只是介绍一下框架,也就是模块之间的耦合,模块内部我放在各次作业内部分析吧。
我三次作业没有重构,继承的都是一个框架,如下图,

 

即input、scheduler(下记sche)、elevator(下记ele)三种线程,而共享对象为两种类三种对象一个是Person(Request)repository(下记prr)类下的两种对象Person(Request)repo_Waiting(下记prwaiting) and Person(Request)repo_InElevator(下记pine)和OrderSlot(下记os,不是那个operating system,Orz希望各位仁兄能看懂)。
另外我为了判断终止线程,建立了一个judgeend类,用以判断和终止线程。

(但实际中因为到后来时间不够了,所以封装性可能没那么好,可能出现类之间越级可见的问题了,比如某个线程通过prr获得pr或p,然后越过prr直接对p或pr的读写操作等等,感觉这样不太好)

下面是组成:
pr和os其实我感觉更像一种数据结构,我在里面定义了adt,即数据的组织形式和相应的api。(我不确定名词用的对不对)

pr:前两次作业由PersonRequest(下记pr)组成,第三次由Person组成(下记p)

os:由Order组成

特意在这里我要声明几个宏(我不确定名词用的对不对)
如下图

 


其他的不用解释,null为电梯从命令槽读走一个命令后,在命令槽相应位置留下的状态(用以调度线程判断是否需要配给新的order)


下面是共享对象与线程的关系:
prwaiting:input对prwaiting专职写,Sche对prwaiting专职读(其实应该叫read and get,也就是也是有写操作在里面的)
os:sche专职写,ele专职get
pine:sche专职读,ele专职写

线程的作用:
(被!!标记的将在下面分析问题)
input:没啥说的,读取输入,进行初步parse然后放入prwaiting
sche:!!所有ele公用一个sche!!,这点没在图中表现出来,所以我单独说一下,负责下达电梯的命令(运动 or 等待 or open)、决定电梯的目标楼层(运动方向),!!决定电梯在该层加载的Person(Request),并放在order中!!
ele:!!只负责读取相应的order,进行对应的反馈!!,唯一承担的非IO类任务就是每到任一楼层判断pine中是否有人(请求)要出梯。

调度器的内部设计:
首先电梯信息存储在调度器中包括当期楼层目标楼层运动状态等。
流程:
1.判断每部电梯相应的orderslot是否状态为NULL,如是继续,否则判断下一步电梯
2.根据当前楼层和等待的请求,判断是否有人上梯,并将将要上梯的人(请求)加入队列prtobetaken
3.根据prwaiting,pine,prtobetaken判断next targetfloor
4.根据prtobetaken是否为空决定是否开门,如不为空用order的构造方法1:new Order(Order.OPEN,prtobetaken)
5.如为空,则根据currentfloor和targetfloor判断运动方向或等待

不同的算法只需要调整2,3的方法

大体框架介绍完了,后面是一些补充的我感觉有问题的地方和分析:

1.由于所有ele公用一个sche,sche感觉用单例模式更好一些???
2.我采取的是在调度环节决定谁上电梯,而不是在电梯到达相应楼层的时候进行判断,我关心的两个问题:
(1)导致在下达命令和电梯相应order之间的时间的发出的本可以被电梯响应的请求不能及时被满足
(2)如果在电梯到站的时候判断加载谁,是否会导致open和close之间的时间无谓的拉长,进而影响效率(虽然后来证明算法的影响远远大于这个,但是一开始我秉承着运算和输出分离的原则,能不让ele运算就不让他运算,保证ele只输出,而在其wait(那几百毫秒)交出cpu后,再进行运算,试图保证输出的时间间隔尽可能近似于理论值)
3.我很关心的ele与sche之间在电梯的状态信息上的共享模式,我采取的是电梯不存储自己的状态信息,只保留输出所必需的信息比如id和currentfloor,而所有的信息和处理集中在sche中,这造成的的一个后果就是sche过于冗长,成为了上帝类。
4.三次作业我主要用的都是在读写某个对象的代码块的局部上锁该对象,因为感觉这样灵活度比较高,可以充分利用cpu不用让其他线程白白等待,但是后面为了保证线程安全所以我锁的范围变大了,有些地方几乎可以保证一个线程按照我的要求跑完一整个流程才交给其他线程,不知道锁方法、锁小范围代码块、锁大范围代码块哪个更好一些,各自有什么优劣???

 

下面是每次作业的分析:
(每次作业跟的架构和上面几乎一样,因此每个类的方法我就不像上次博客一样一一介绍了,实在是没什么意思,这次blog对于每次作业主要分析性能以及介绍一下遇到的问题。)
2.第一次:
介绍:

 

 


分析:
性能:
可以看到,耦合度、复杂度等等还算可以,只有nextOrder因为融合了一部分nexttargetfloor的功能,所以结构化差一些。
架构:
感觉没啥说的,框架就是上面的框架,但是prr存的是pr,也就是共享对象是请求不是人。
cputime:
我用的小轮询,也就是调度器是while(true)里面加sleep,本意是想让输出之间尽可能不要有运算,以保证接近500ms。
线程安全:
这次我犯了一个巨大的错误就是在锁的对象不是共享的slot,而是order(这个其实没有共享的问题),但因为只有一部电梯而且小轮询的时间控制,所以出问题的概率很低。
算法:
本来想优化的,但因为发现总是能针对某个算法构造出性能极差的点,在不知道强测原来是随机数据的情况下,我怕以后的作业会炸掉某些点,因此我为了第二次作业好改,所以写的fafs。

3.第二次
介绍:

 

 

分析:
性能:
这次因为一些原因周二上午10点我猜开始动手写的,因此一些设想很好的东西在实现的细节上不是很好,耦合度、复杂度可能在数据上上升的不是很明显,但从我内心的感觉,为第三次作业问题的复杂化,埋下了伏笔。
可以看到也还可以,这次因为我把nexttargetfloor从nextorder分出去了,所以结构化强了一些,但是对电梯run我修改了判断结束的方法和修改了输出的一点结构,导致耦合度上升了,这里确实我当时面临着停车写错了导致停不下来的问题,小轮询cputime炸了,需要改成wait/notify的问题,以及新增的arrive和捎带问题,我没有想怎么降低耦合度(时间来不及预先的结构优化了)
架构:和第一次基本一样
cputime:
改成了wait/notify所以正常了极端情况也在2s以内。其中ele在wait前会唤醒sche,但sche不会唤醒任何人
线程安全:
其实这个也不算线程安全问题,但是实在共享对象os上连续第二次出现的问题,就是在第三次作业中,我发现第二次虽然锁的是slot,但是锁的是大家公用的slot,形成了一个sche对一个os进行put,n个ele对同一个os进行get,造成任意一个ele试图唤醒sche的时候,都有很大的可能唤醒的是本不该唤醒的其他ele(这点我想知道有没有比我更好的解决办法),我选择的是每个ele开一个相应的slot,每次ele的操作只锁自己的slot,如下图

 


算法:
我还是不知道数据是随机的,我怕look或者其他的算法会炸,唯一能保证我不会有点炸的就是als,所以我老老实实地用了als(后来才知道数据原来是随机的Orz)

4.第三次:
介绍:

 

 


 

分析:
性能:
首先,可以看到run方法的问题被带进了这次作业。其次,Person的requestparser方法的复杂度很高,主要是因为我将出发楼层划成了很多块,过多的if-else结构造成v(G)过高。而最致命的就是nexttargetfloor方法了(捎带着把scheduler的run方法的非结构化程度也带起来了),这主要是look+满载+只能停靠部分楼层,一开始我没有构思好,当时也没有用工具分析复杂度,只是一开始我的设计就是降低耦合度,即所有作业主要的工作只需要修改这个方法,结果第三次都写在这个方法里面了,甚至发生了方法过长,以至于我要用面向过程的方式拆分出几个函数单独作为private方法,本意是好的,但全都堆在这个方法里面的结果,就是该方法的复杂度和耦合度极高无比,也直接导致了我没有捋清思路,进而导致我出现了很多细节但是致命的bug,并隐藏了一个由几个非致命bug组合在一起时发生的致命bug,导致我的作业在强测中爆炸。
架构:
和第二次也基本一样。这次我把输入构建了一个person对象,对象里面将request根据(from,to)矩阵,固定了分割方式,形成一个队列,并进行了封装,最终只有前面的子请求执行完成,后面的请求才会暴露给其他类。
cputime:
一开始遇到了,ele会唤醒ele的问题,但是按照在第二次作业的线程安全部分介绍的方法,我解决了这个问题。
线程安全:
这次没有什么线程安全,还是按照上面一条进行了修改,其他部分都没怎么动,就是在sche和ele中锁住的对象换了一下。(这三次作业我感觉我没感受到助教常说的线程安全问题,所有问题基本都可以复现,除了投放和停止的时间不太好卡,以至于我怀疑我的方法是不是有问题,是不是到后来我怕线程安全有问题,代码块锁的太大了,这会不会带来什么不好的地方?我这么做有什么更好的解决办法?或者是各位在线程安全中遇到了什么问题,这都是我特别想交流的地方)
对于notify问题我还是按照课上推荐的notifyall,虽然好像除了prrwaiting,基本上每个共享对象只被一个生产者和一个消费者共享,而且也没有细研究过如果真的用notify,prrwaiting会出什么问题,但是确实要注意的是我的sche只wait不唤醒,但是我的ele只会wait最长xxxms(其实也不能说最长,应该说是一定时间后自动唤醒),而input线程根本不wait or notify 因为我只锁了很小的一部分,所以我感觉也不会有什么太大问题(这个也是一个我很困惑的点)。
此外我特别担心死锁的问题,我一直没太关注,也没遇到过,所以特别想听一下大家死锁的经历(比如原因,和解决方案,如果有兴趣的话我希望能在下面交流,虽然我可能不能及时看见并回复Orz)。我看了一下,我没遇到死锁的原因,我分析主要是:
1.只有os,prrwaiting,pine需要上锁,其中只有sche和ele需要对多个对象上锁
2.多把锁我都是第一把是os,所以是不是因此造成的不会死锁
(我比较水,所以我不确定想的对不对,死锁问题,因为比较幸运一开始其实我没有想到,但我的设计我觉得可能是因为上面的原因,所以一直没暴露出问题,因此我很慌张Orz,希望能和大家交流)

 


这是一个最极端的例子,我用两个锁锁住了整个方法

算法:
这次我用的look但是一个地方忘了判断方向了,本来是同方向跑到最远的,现在变成了优先上行(配合上其他看似非致命的bug,在后面要讲到的一个情况下,造成了巨大的bug),导致bug+性能分归0。以及一个地方我的break居然写在if块的外面while块的里面了Orz,导致循环只能完成第一个循环,于是我的电梯一定程度上变成了fafs,也就是第一人的第一个请求不为该电梯所能执行时,该电梯不能相应其他请求(Orz 10^1级别的秒数的性能额)


三.自己程序的bug
其实在第一次的线程安全和第三次的算法里面已经简单介绍了。
1.第一次作业,线程安全问题,因为锁的对象不是长期存在的(当时是锁错了),会在一波操作后被jvm释放(也就是没了,对象都没了,锁还有什么用),但是因为我的sche会以比ele相对快50ms的频率小轮询,所以硬生生被我拉成了近似单线程。。。也就是ele跑的时候sche大概率在sleep,sche跑的时候ele在sleep(其实从这里我就应该发现sleep是不交锁的,因此一定是锁失效了,但是当时还是太年轻没意识到,直到第二次作业改成wait的时候才发现没有获得锁)。但因为50ms有可能被误差抹掉,所以在互测中被hack了一刀,但不知道为什么bug修复界面没算,可能因为bug几率太低,评测机会复查,结果没跑出来吧。(如果不是评测机或者前端出了bug的话,我也很想知道为什么)
2.第三次作业,bug因为nexttargetfloor的超高复杂度,所以非常多,而导致我性能分和强测爆炸的是如下三个bug的共同作用:
(1)电梯在满载时依然会对prrwaiting做出相应
(2)look没判断方向,被写成了优先上行
(3)break直接写在了while块里面,造成不分时候fafs
(1)(2)共同导致了满载并折返时,我的电梯会震荡。最终修改我只修改了bug(2),虽然剩余两个bug依然在影响我的性能分。

这三次作业每次都是一波极限操作,因此我没有写评测机,尤其是第三次作业只写了一个随机生成测试数据的脚本,因此输出需要我肉眼识别,也因此我没有测试请求达到饱和的点(bug1与上bug2),也没细看电梯过程中的电梯走位(bug1或bug2或bug3),也没捋清所有的可能以遍历所有情况进行构造,因此没有查出来如山的bugOrz。

四.分析别人的bug
之前都是用评测机这次用的随机数据+我跑出来的结果,这次我没有仔细研究所有情况,所以真的没有什么策略了(敢于自爆家丑,希望助教大大不要因此扣分),前两次,怎么跑都没跑出bug来,第三次随便一跑就能一穿四Orz。这三次都没有什么特别好的构造,都是随机数据,前两次写了一个简单的测试脚本用来判断电梯是否在正确的楼层取人放人以及是否有没接上的和关在电梯里面的,但是过程中是由符合时间差以及其他规则我则没有判断。

五.心得体会
1.线程安全:我觉得三次电梯主要考我的是算法,线程安全真的没有给我留下太深刻的印象
2.设计原则:我觉得这次高内聚低耦合还是做得不错的,但是后面的作业为了沿用之前的架构,还是出现了上帝类(scheduler)说明架构的可扩展性还要提升(当然了也有我机械地只专修改一两个方法的原因,其实把这两个方法解决的问题,根据高内聚低耦合的原则(由于之前的设计已经做到了低耦合),重新在局部构建一个新的小架构,结果会好得多),以及多态和继承、接口什么的还是没有熟用。
3.从SOLID角度评价上述介绍:
S:还是不错的
O:还是很不错的
L:不太好,这次我机械了,连继承多态这些东西都没想起来用
I:还可以
D:也还可以

六.其他
(这次为了能在上面多写一些细节,所以我组织的不太好,可能有些凌乱,不过我真的是认真写的,也希望能多交流,给他人启发或教训,给后人经验)

posted on 2019-04-23 00:35  gchl  阅读(137)  评论(0编辑  收藏  举报