301
社区成员
发帖
与我相关
我的任务
分享一个小破烂
哪怕这一届赶上了好时候,难度降低了,完成第二单元也不是一件很容易的事情。
实话说,这一单元完成度并不高,三次强测分数并不尽如人意,互测和强测也都有bug。锁还是学得半懂不懂,线程安全不知道学没学懂。哪怕是最后的最后,还是因为调度问题不得不用回模6实在有失调度器尊严。不过学完这一单元确实收获良多,简要总结如下。
主要结构如下:


第二次作业为了传递reset参数加入elereset类并在request类当中加入调度器。
第三次作业加入doublele,doublelist,doubelthread,occupy类完成双轿厢任务
由main类实例化elethreads容器和requests。
Input类读入请求并区分是乘客请求、电梯reset请求还是DCreset请求。
如果是乘客请求,给requests类进行分配,加入对应list供电梯进行运载。
如果是电梯reset,给对应elethread进入reset程序。
如果是DCreset,给对应elethread进入reset程序,结束后结束进程,生成新的doublele、doubelthread、doublelist并加入requests进行管理分配。
这次是一堆结构精巧但是跑起来破破烂烂的屎(闭眼)

可以看到,仍然存在不少方法有复杂度高,非结构化程度高,模块耦合度高,架构不清晰,很多方法堆在一个类中但没有很明确的聚类关系,没有完全满足一个类一个作用的单一原则。意即我无法回答为什么这个方法应该放在这里而不是其他地方,因为确实有一些方法可以放在单独的一个类里面,但是因为种种原因没有实现(就是懒吧)
相比第一单元,架构有所进步,但是仍然需要改进。
(自由竞争真的很好用啊!
三次作业是一个由屎到屎的过程,又名《我和notify、锁和捎带策略搏斗的一生》
在第一次作业当中,我认真学习了学长的博客,选择了自由竞争+量子电梯的结构策略,考虑到指定电梯,我放弃了自由竞争的get(0),选择了遍历找第一个指定该电梯的乘客,又为了卷性能分,在list内部选择了LOOK算法。这也是魔鬼三周的开始。
· 量子电梯
量子电梯是一个钻输出空子的小技巧,不知道为什么会有这么一个高大上的名字。其基本思想在于,在开门关门上下楼层的时候,只要求间隔一定时间输出结束(或者开始)的指令。其中的具体过程仅仅通过输出是不得而知的。举个例子,假设一层楼开门关门和到达下一个楼层的时间间隔是0.4秒,那么只要能够保证ARRIVE和CLOSE的间隔是0.4s就好,至于期间干了什么,只要不输出,没有人知道。由此量子电梯诞生了。

量子电梯提升了一部分电梯的运行速度,算是在卷性能分时代价很小的一个方法。不过其缺点在于,由于跑的太快了,在ARRIVE之前时间段来的电梯会捎带不上,导致性能分的缺失,对于短时间涌入大量数据的情况不占优势。对此,曾经的一个实现是在接收到请求时停留10ms判断是否还有请求要进去,我大为震撼并连连膜拜,最后也没能实现(一实现就从停留10ms变成一直停留,好吧!)不过第一次作业的强测结果显示似乎这么做的代价有点大,遂放弃。选择用调度策略来弥补
第二次作业加入了reset,要求在reset期间不能够receive请求。我最初的实现是当电梯reset时选择其他电梯,后来发现这样的效率太低,如果reset的电梯太多,最后会变成单电梯甚至无电梯,于是加上如果无电梯能够receive就等待,不知道为什么无法唤醒。最后在大佬的帮助下,选择如果reset,则先将分配的请求暂时存储到一个新的容器中,等reset结束统一输出。(怎么又在钻空子)最后的强测和互测证明,这么做比较明智。分给没有reset的电梯的策略被hack得很惨……
第二次作业还加入了调度分配策略。最开始,奋斗三小时我要过清明,拿模6写完了。这里要注意模6的对象,我是拿加入的请求数量模6,是很均匀的分配。有人拿乘客ID模6,研讨课据同组同学分享,这样的分配方式很容易debug,不过互测被hack得很惨(50秒,70条,reset,3个人的容量,全部模6余a的乘客ID)后来担心互测和强测tle,还是改成了一定的调度策略:捎带+最近的空电梯+请求最少的电梯。事实证明,这里的实现还是有一定问题,放在bug分析里详细写。
第三次作业加入了双轿厢电梯,其实现难度在于如何确保双轿厢不会在换乘层撞车。借鉴了讨论区同学分享的方法,我给两个电梯线程设置了一个共享对象occupy,当且仅当两个电梯都想进入换乘层时,后进入的电梯会wait直到被唤醒(和操作系统进程与线程部分的进入、退出临界资源思路好像啊(胡言乱语))
waitRelease(); \\线程wait直到state不为1(即换乘层没有被occupy)
state = 1; \\表示换乘层被占据了,对方电梯此时若需要进入换乘层需要wait
为什么要先waitRelease再设置state为1呢,就是因为,如果在waitRelease时state不为1,表示换乘层没有被占据,自然也不用wait。如果改过来就百分百会wait,导致tle。
(放过我吧我不想再写了)
如果非要说扩展,或许可以加一个横向电梯系统,需要保证横向电梯和纵向电梯不能相撞
既然都引入共享对象和同步互斥了,那就让暴风雨来得再猛烈一点吧!
再歹毒一点,建议把输出改一改,少钻空子多想办法(不是
·你轻轻一写我痛苦三周系列
·真好啊第三周了终于搞懂了为什么notify唤不醒
·为什么notify唤不醒wait的线程是因为不是命定之人没给真爱之吻吗
大红大绿的真好看代入感很强已经在东北穿大棉袄看二手玫瑰了
官方数据投喂包用起来怪怪的,感谢助教的TestMain使我可以直接在IDEA里面运行程序,可以直接使用ItelliJ Profiler查看时间线。如果不是万红丛中一丝丝的绿,那么恭喜你轮询了。
轮询的本质原因是notify和wait不恰当,要么唤醒太多,要么wait太少。一般而言是前者。这就涉及到,notify的对象问题了。
notify的对象是什么?经过我第三周的孜孜不倦(指坚持追问chatGPT)我的理解如下:notify会唤醒在本类对象上wait的线程。简言之,所有调用了该类上有wait和synchronized方法的线程都会被唤醒。因此,做好wait和notify的架构至关重要,我个人倾向于不同线程有各自的wait和notify,放在我的架构中来看,就是电梯线程所私有的list里面进行wait和notify。
最大的经验是如果不知道该怎么加锁可以全部把共享的东西都加上试试,如果有死锁再删(扫雷大作战好诶!)
synchronized可以修饰两个东西:方法和对象,本质都是:上锁。给方法上锁,是说当执行该方法时其所涉及到的所有东西都会被上锁。而给对象上锁仅仅在执行到语句
sychronized (object) {
}
的时候才上锁。显然,后者的锁法更灵活、更方便,其对象更新更快。
所以当不知道该怎么加锁的时候,另一个方法是什么都不加,跑一组超强数据等着IDEA爆红(谨慎使用,不保证正确性OO大赌场)
借用OS的信号量控制同步与互斥的思想,给方法加锁类似于同步化,执行该方法的时候确保所有涉及到的对象都是最新版;给对象加锁类似互斥,确保括号内语句执行时不会被其他方法改变。最常见的例子是,对于双轿厢电梯,我选择的实现是加入两个新的线程到doublelethreads容器当中去,从而加入分配。这就意味着,当两个及以上的线程都在分裂时,会出现一边遍历doublelethreads一边在往里面加入新的线程的问题,这时候就需要上锁,否则就会出现并不按照AB顺序加入容器导致判断失误、无法到达请求出发层的情况。
我遇到的tle大致分为两类:请求完成无法正确结束,和请求未完成无法正确结束。分别叙述如下。
还是wait和notify的逻辑不对。
按照我的架构,当目前没有请求时,线程陷入wait,等待输入结束时notify唤醒,进入新一次循环,进入判断,满足条件则结束。所以又回到了那个熟悉的问题,“为什么唤不醒呢为什么呢是死了吗”。当正确安置notify和wait位置时,该问题解决了。
听起来很正常,对不对?请求未完成,当然无法正确结束。但我遇到的问题是,它既不完成请求,也不正确结束。
真好啊notify又是你
这个错误主要集中在后两次引入reset的作业。
意即当已经完成输入,由reset电梯或者双轿厢的A电梯抛出来的请求因为不够及时导致本来应该加入到的线程已经结束,造成tle。因此,对于加入请求和完成请求,都应该及时改变todo数量并notify所有线程。
另一个情况是对于在最后reset的电梯,请确保分裂为双轿厢时电梯进程继承了原电梯进程的状态量:isEnd,isFinish等
血泪教训:不同的notify对应不同的wait,因此,notify一定会奏效。如果你的notify失效,请考虑线程是不是已经结束了(思路打开
小丑来啦
如前所述,采取的调度策略为捎带+最近的空电梯+请求最少的电梯。其bug在于,如何界定最近的“空”电梯和请求最“少”的电梯。
空电梯指电梯内和list当中都没有请求,那么reset期间临时存放的电梯呢?DCreset期间临时存放的电梯呢?请求最少同理。
此外,如何界定最“近”?我没有考虑reset时的1200ms和理想情况下去接送乘客(即中途没有任何停车)需要的时间来判断来判断最近。导致最终50s大量乘客的情况下只有一个电梯在跑tle。最终不得不改成模6(真小丑啊)不过模6的性能分还是不如调度,hw7甚至五六个85,谨慎使用。
几个比较好的思路:
· 捎带+请求最少+最近:当请求数量相等时时考虑距离(即所花时间)
· 先判断是不是双轿厢,如果可以就直接扔进去。再判断普通电梯与请求的距离。注意利用双电梯耗电量低的特点,性能分在卷时间也可以适当关注耗电量
· 先看同方向有没有捎带,没有再依次看上下12345层有没有来接的
疯狂打print,看一个语句是否执行就在前后都打print。有时候可以将ARRIVE等先注释掉减少筛选量。有时候看一个电梯一个请求的执行情况可以加条件输出
if(ID == a) {
print()
}
记得在提交时该删的删,不该删的别删!!!!
对于死循环,请及时终止,避免因为输出太多覆盖前面输出和占满堆栈内存导致IDEA变得很卡
轻舟已过万重山,一山放过一山拦。最难的一个单元已经过去了,希望接下来一切顺利吧!
无需多言
停下来想一想问题,看一看资料,学一学概念,问一问周围,交流一下感想,有时候会有意想不到的效果。