面向对象2022-第二单元作业总结

面向对象2022-第二单元作业总结

第一次作业

  本次作业是在单栋楼中设置一部电梯接送上下楼的乘客。

(1) 同步块设置&锁的选择

  这次作业是多线程作业伊始,能否处理好锁与同步块之间的线程安全,让不同线程间可以共享数据并且保证安全,对多线程作业的发展具有重要意义。在对题干进行一番迅速却又不失严谨的推理后,我很快从乱如蛛网的诸多线索中推理出这样一条至关重要的结论——每栋楼层里只有一部电梯。

  前路一瞬间就变得豁然开朗了,当一个楼里仅仅有一部电梯时,队列不会被多台电梯竞争,换言之,本次作业想要出现线程安全问题其实并非一件容易的事。事实上,可以直接对Controller加锁(Controller是本次作业中的类,用来存放乘客,可以理解为楼座,实际上并没有控制功能),保证电梯和InputThread不发生冲突就好,另外还有加锁的地方就是输入结束的IsEnd信号量。至此,加锁的地方其实只有5个(都是上有的两种情况),并且由于单部电梯直接给对象上锁也不会影响性能,本次作业很稳。

  不过这里还暗藏了对于锁的选择,为什么会在这些地方上锁呢?条件1,同步块种涉及到对共享变量的读或者写,可能由不同的线程进行。条件2,这些共享信息很重要,可能一旦判断失误将会引发死锁。

(2) 调度器设计

  本次作业中选择让电梯学会自己独立思考,而不是调度器云亦云。因此,Controller只是一个存放乘客和各种信息(即各种共享变量)的地方,电梯要懂得去看Controller当中的信息,并开辟自己的前路。

  首先Controller本就有一些信号量,电梯可以在初次判断后决定是否遍历,而即便是在遍历时,也是要结合自己的行动方向,可继续承载量来装人。由于本次作业每栋楼里仅有一部电梯,不存在多部电梯竞争,加之我想要教育电梯之灵的良苦用心,在本次作业中电梯还是成长到了独当一面的模样。

(3) 类图与复杂度

 

 

(4) 实验历程

  本次实验是首次实现多线程,对我来讲入场主要分为两个阶段。在上一阶段大量阅读多线程有关知识,包括sleep,wait,notifyAll各种函数的作用和使用方法,但并没有将其纳入到电梯实现中。首先解决的还是电梯运行的一系列基础问题,简单来讲,就是拥有电梯的物质条件,至于多线程运行则是电梯调度的事。

第一阶段内容如下所示:

1. 让电梯可以动起来,设定了层数,并且可以改变。
2. 让电梯可以接受信息,根据信息跑起来。
3. 电梯在初始时接受了两个人,这两个人前往不同楼层。
4. 有人在第5层按下了电梯按钮,电梯可以去接人。

  至此,电梯完成!我们拥有了正常的电梯,开始引入线程和调度器。由于是初次接触多线程,还是将目标分为多段。

第二阶段内容如下所示:

5.将电梯设为线程模式,可以处理在一开始输入的值。
6.可以让controller在不同时间段输入值。

  现在电梯本身的功能已经可以实现了,但需要优化的内容还有很多,考虑的问题如下:

1. 同方向用户捎带问题
2. 电梯进程停止设定问题
3. 电梯恰好停在某一层时,有人按电梯
4. 电梯超载问题
5. 潜在的死锁问题
6. 性能优化问题(调度算法)

  本次电梯的调度是仿照了宿舍的电梯运行方式,尽可能地到达同方向最远的地方,同行捎带等(在未超载前提下)。代码中比较精妙的设计是变量toFloor,即目标楼层。其解决的主要问题是总是能够合理地接人。

  试想这样一个情景,电梯将某个1楼的人送到5楼,在开门送出该乘客后,发现5楼有往下的乘客,是否应该纳入呢?按照仅接纳同方向乘客的逻辑,是不应该接的(此时电梯方向向上),可如果5楼以上的楼层没有请求了呢?这时电梯将会关门,检测外部是否有其它请求,在发现5层有人往下后,更改方向,再次开门,并且接纳用户。一切看起来理所当然,但现实生活中可不会出现某台电梯在你面前先关门再开门的情况。这个开关门的操作,至少损失0.4秒,而评分会有性能分的。

  或许解决这个问题很简单,不是仅接纳同方向的人,每到一层接纳所有乘客,然而这将触发超载和电梯空间浪费问题。

  换一个角度,问题根源在于方向的设定。首先将外部的人接入,然后进行判断。当curFloor == toFloor时,重新设定toFloor,并再接一次人。巧妙的是,设定toFloor的函数是结合内部、外部请求和运行方向判定的,如果toFloor的新值仍然为curFloor时,更改方向。另外补充,当curFloortoFloor相同时会退出电梯移动的循环,重新进入时还会设定一次toFloor的值。你不妨构想各种情况来攻击此处的设计逻辑,此处不进行赘述。

  或许我还应该讲一下线程的相关问题。但第一次作业比较简单,只有一台电梯,需要上锁的对象也只有一个Controller,因此相关的设计并不复杂。在适当的地方上锁,增加wait,notifyAll即可。进程本身带来的复杂性可能要到后两次作业才会真正体现。

 

第二次作业

  世界上突然出现了一种名为横向电梯的生物,至此,楼与楼之间连接起来,静态变量的优势也有所体现。而乘坐电梯的人也发现,改革开放提高了人们的生活质量水平,每栋楼每一层开始有了多台电梯,并且电梯数量还在增加。

(1) 同步块设置&锁的选择

  横向电梯与纵向电梯在本质上似乎没有什么变化,只是我为了将乘客装在Controller类里,设置了一些静态信息变量使得在进入一栋楼时就可以知道另外四栋楼的信息。

  不过另一个问题是多部电梯出现后,为了保证性能,原有的对整个Controller对象上锁被精确到了对具体的队列上锁,落实到代码中就是在InputThread加入乘客和电梯取出乘客时的两个地方仔细考虑,而新增乘客时也会notifyAll唤醒一些沉睡的电梯。

  加锁的逻辑是不让critical出错,而同步块尽量小则是为了性能。

  值得一提的是,这次作业引入了读写锁来提高性能,并注意到读写锁必须要“手动释放”,使用wait不会自动释放读写锁,离开函数也不会释放锁。事实上,这里很容易造成死锁或者看到程序报超过锁数量上限的异常。

(2) 调度器设计

  乘客蜂拥而来,电梯该如何应对?是选择放弃思考,向调度器寻求安慰,还是增强完善自我,抽刃向更强者。面对电梯的眼泪(重构的命运),我选择了后者。可这时又面对一个问题,电梯之间需要相互沟通吗?一个乘客似乎只需要一台电梯接送,如果自由竞争可能导致出现电梯接不到人的情况。

  但这并不重要,还有在那之上的东西,那即是乘客的幸福感。当忙碌疲惫的乘客在高层按下电梯时,看到多台电梯竭尽全力来接他时,他会感受到关怀和满足。反之,如果每次都仅有一台电梯向他移动,别的电梯冷漠不动,他或许会黯然神伤。

  本次选择了让电梯自由竞争,而这其实也带来了其它好处,比如只需要在电梯中新加小小的改动,判断目标楼层是否还有人,如果被其它电梯接走就重新计算目标。而别的复杂情况,如一台电梯去接人时突然加入请求,捎带机制导致初始目标无法接到(电梯满载)。另外,在同一层加入乘客过多,一台电梯接不完,判断需要几台电梯去接也很麻烦,而自由竞争则避开了这些问题。

(3) 类图与复杂度

  可以看到第二次作业的类图相较于第一次作业主要是增加了横向电梯CrossLift,同时Controller类增加了对称性的调用CrossLift的函数。

 

 

  毫不出人意料的,电梯类和控制类的复杂度都很高。而其中最了不起的当然还属run()方法。run()方法中需要做的事情有很多,一些功能也被该类中的其它函数所解决。但之所以没有进行简化,其实还是因为评分机制。性能分是用性能来判定的,因此为了尽可能的节省时间,就不使用函数进行跳转。不过这样做应该也并不会带来过多的收益。

(4) 实验历程

  本次作业在增加了电梯的数目,并且在纵向电梯的基础上增加了横向电梯,基本可以延用纵向电梯的思路,但还是注意以下一些问题。

 1. 横向电梯如何访问数据,如果每次访问都要遍历每栋楼的信息,那么需要调用5次跳转函数。
2. 锁的使用。在一次作业中可以对函数直接使用`Synchronized`,但在访问同一对象中不同属性的线程多了之后,为了提高效率需要需要采用代码块形式的锁,并引入读写锁技术。
3. 向第三次作业的迭代。如果第三次作业有换乘机制的话,那么需要尽量地方便数据传递。

  为了解决这些问题,没有将楼层分为两座,即横向乘客和纵向乘客分别使用的是不同的楼,互不干扰。楼层还是只有一座,只是将内部乘客进行了不同类型的划分。另外为了使横向电梯在进入一栋楼的时候可以直接知晓其它楼座的信息,因此将某些关键变量设置为静态变量,以方便所有的楼层进行数据共享,也方便了数据的传递等操作。然而缺点就是楼层的功能日益复杂化。

 

第三次作业

  个性化电梯诞生,同时人们学会了换乘。

(1) 同步块的设置&锁的选择

  本次作业的同步机制与第二次作业设计相同,只是为了支持个性化,对判断的信号量进行了修改。至于换乘问题,并没有触及到线程安全。换乘只是在送出乘客时将乘客重新放入到队列中,既然InputThread不会造成线程危险,那么电梯在加入乘客时也不会。

(2) 调度器设计

  同作业二,电梯间自由竞争。而乘客则承担了道路选择的任务,在每次进入Controller前都会使用thinkFutrue函数来决定下一次乘坐是横向还是纵向。当在电梯释放乘客时,则会判断是否到达真正的目的地,如果没有的话则再次thinkFutrue并加入到Controller中。如此一来,作业三就直接延用了作业二的架构。

(3) 类图和复杂度

  第三次作业和第二次作业的类图几乎相同,只是额外引入了Rider类作为过渡,使得PersonRequest的功能更多了一些。

 

 

  有意思的是,每次作业的平均复杂度一直呈下降趋势,这说明多写一些复杂度低的函数可以拉低平均复杂度。

(4) 实验历程

  第三次作业主要难题有两样。一样是实现电梯的个性化,另一样则是支持换乘。

  个性化指的是允许电梯有不同的速度,容量,访问楼座(层),这只需要让电梯运行时的睡眠时间,最大乘客数等属性可以根据电梯请求的信息变动即可。但是,横向电梯的访问楼座却并不是那么容易对付,除了需要将其设计成电梯的属性之外,还要有数据的架构支持。

  举个例子,当横向电梯只能支持在A,D,E三座停留,而不能在B,C停靠时,那么电梯就必须做到只访问A,D,E三座楼的信息。但是A,B,C是三个不同的对象,如果是逐个来判断楼栋状况,那么在判断B的状态时A的情况发生改变了该怎么办?可以选择将A,B,C三个对象同时锁上,但这样做不仅使得控制难度加大,并且性能也降低了。另外,判断楼层状况也并非只是观察该栋楼里是否有乘客,乘客将要去哪也是个问题。当A座的乘客想要前往B栋时,前面提到的横向电梯也不该去接。一种想法是在观察楼栋状况时更进一步遍历每个乘客的目的地,但性能又会成为问题。在这次实验中为了支持这种个性化设计,又不至于损失性能,额外使用了类中的静态量来表示不同楼栋不同楼层的状态,并为此设计了一些算法。

  支持换乘是一件无比复杂的事,也是一件相对简单的事。其困难在于寻求最优解,电梯状态包括当前装载人数,最大人数,当前楼层,移动速度体现出当前电梯的运载能力,然而每栋楼又不止有一部电梯,因此每栋楼的运载能力是所有电梯运载能力的复杂加和。除了电梯又还有乘客需要考虑,每栋楼里人多人少是两件事,每个乘客的目标楼层有所不同,另外性能分计算的是所有人的等待时间,处理乘客也很麻烦。最后还有动态性,换乘路径并不等于最短路径,流量对一条路径的运载能力也将有影响,并且由于乘客和电梯都可以增加,最佳解是动态变化的,问题进一步加难。

  最后,还是选择了看待支持换乘的简单的一面。按照标准策略在乘客加入时确定未来换乘路径,而所谓换乘就是将乘客送出电梯时再次判断看是否需要再放回楼座即可。实际上还是蛮简单的,只要做一个乘客类可以修改目的地即可。有些不明所以的人以为这是偷懒,但面向对象课程的核心在于架构而非算法,完成作业时本就不应该舍本逐末。

 

架构分析

 

  由于本次作业在一开始就选择了自由竞争,故扩展的方向比较有限,难以支持比较复杂的调度。或者换句话说,扩展空间相当大,可以支持从自由竞争向理性调度的飞跃。

  纵观三次作业的变化历程,一开始是电梯使用look算法不断接人,然后是确保多台电梯接人时不会引发线程安全问题,最后则是让电梯可以更改属性以及让乘客可以变化。而架构也比较简单,一部分是InputThread不断加入新的乘客,另一部分则是电梯不断满足乘客的请求,最终当请求全部满足时则让所有电梯的线程终止。

  如果谈当前电梯可以比较容易支持的变化的话,让纵向电梯只能前往特殊楼层比较容易,实现横纵向皆可通行的万能电梯也并非不可。但更多的变化,比如接人的偏好性(VIP或者人群多的优先),还是需要引入新的类来实现,继续扩展电梯的功能是不明智的。

 

bug分析

  第一次没有hack别人,房间里的总hack数也仅有7次。我想应该是互测数据要求过高,试了几次后没有交上去,想想hack也不一定能得分,还是做一些别的课程预期收益更高,就懒得hack了。强测和互测都没有bug,不过性能可能还是要改进,有两个点扣了2.5分以上。

  第二次作业给了我很大的教训,使得我对线程不安全记忆深刻。电梯的输出中有两个位置使用了线程不安全类TimableOutput,结果不言而喻。但深入来讲,找到这一个bug并非易事。首先,逻辑和功能都是实现了的,不仔细看的话很难注意到有问题,何况阅读代码时主要的注意力也是放在了前后逻辑和死锁上。其次,在评测前提交了数次中测,并且还试了几次互测的强测,都没有暴露出这一个问题。针对这一类bug,暂时没有想到好的应对策略,只能说在运行中发现。

  第三次作业没有发现bug。

  本次作业中并没有怎么hack别人,评测数据的要求比较严,直接提交中测数据也无法通过。但根本原因还是时间比较紧,没有在debug上花太大心思。

 

心得体会

  线程安全方面,线程安全问题是不同的线程访问了临界区并有读和写的关系,因此需要通过加锁来进行保护。并注意使用notifyAll语句。然而加锁有时也可能触发死锁,过多的notifyAll也会导致轮询占用CPU资源。一些同学总结出了在共同读和写的地方上锁,而notifyAll则是在执行了写操作后使用。在初步入门线程问题时可以这么理解,但现在的我觉得,程序员应当尽可能确定缺乏锁会导致什么情况,而notifyAll唤醒的对象以及情形又是什么,而不只是简单的套用结论。一些场合会读有可能被修改的内容,但为了性能并且不发生逻辑问题时可以不考虑加锁。而有的情况又比较神奇,可以光明正大的修改临界区域的值,并且没有直接上锁。总之,程序员还是应该清楚自己正在做什么。

  层次化设计方面,我认为本次作业并不是很出色。原因在于没有系统学习过多线程架构的有关理论知识,只是停留在初步的理解上做问题,这就导致了后期的扩展性并不够好。我认为,第三次作业换乘的要求绝不仅是有具体的算法,关键还要有合适的数据框架。否则,信息之间流通困难,电梯不容易被控制,状态也不容易得到及时监测,而调度互乘自然也就很难实现。因此有空了,还是要多学习相关理论知识,并找到恰当的机会来实践。

 
posted @ 2022-04-26 21:21  早点明安  阅读(79)  评论(0编辑  收藏  举报