301
社区成员
发帖
与我相关
我的任务
分享同步块与锁的选择,关键在于确定会被线程共享的资源有哪些。在确定共享资源之后,要根据被共享资源的访问特性(读者/写者的数目),选择合适的“锁”。
RequestTable对象作为共享资源。这两个对象的读者和写者均各有一个,因此只需要保证“写时不读”,采用对读-写方法加同步锁synchronsized进行保护。 resetSign,采用在读-写方法加同步锁synchronsized进行保护;同时,reset请求到来时,电梯将内部乘客和等待队列里的乘客写回主请求列表,主请求列表变为1读多写资源,可沿用第一次作业中的保护方法。 为了实现临界区的充分小,采用以下的几种策略:
synchronsized的方法,保证其内部仅有简单的原子操作,如加入队列/返回队首元素等。建立了调度线程,通过与输入线程共享主请求队列获取请求,并执行对应的策略,将他们放入与电梯线程共享的等待队列中。
在编写代码的过程中,尤其需要注意的是线程的终止条件。
input结束 && 计数器为0 && 没有待处理的reset请求作为结束信号。第一次作业:直接分配。这次作业中,每个乘客的请求中给定了需要搭乘的电梯,只需要按要求将其分配到对应的电梯即可。
第二/三次作业:公式法(调参法)。其核心思想在于通过电梯运行时的状态参数(如当前所在楼层与目的楼层的距离、运行速度、载客量、等待队列的长度等),建立合适的评估函数,对几个电梯进行评估,最终选择评分最高的电梯。
这种方法实现较为简单,效果取决于评估函数,以及测试数据的覆盖率/强度,但从极限的考虑来说,一个真正好的函数应该可以超过影子电梯达到调度的真正极限。
具体来说,函数的建立分为两个阶段:确定函数形式与确定参数取值。第一,函数的形式上,一般不选择线性函数(这与实际相关,即考虑边际效益递减),但函数形式依然复杂多样,需要更多次的尝试。第二,参数取值的调整,需要借助工具,多多调整。这一步需要大量的数据作为支撑,否则可能造成“过拟合”的问题。
调参法最大的难点在于,需要一个较为成熟的调度器作为比较指标,以及需要大量数据的自动测试。当然,如果有可能的话,可以尝试利用机器学习的方法来解决调参的问题(研讨课上小组成员所提到)。
最后,评估函数不适宜过于复杂,过于复杂的函数往往难以调试,且容易造成针对特定数据的优化(大坑)。力推写一个好一点的自动调参机。
被ban掉的绝妙实现:自由竞争。这种方法借助了自然界中“物竞天择,适者生存”的思想,每当有乘客提出请求时,所有空闲(或顺路)的电梯同时向请求的楼层运行,到达最早的电梯将接到这名乘客。这种方法实现简单,但是由于本年度OO课程新增的RECEIVE请求,以及性能分对耗电量的限制,导致这一经典方法被ban掉了。
调参是世界上最好的方法
调参实现难度极低,同时任意一个较为合理的函数都能达到较好的效果,投资-回报比高。但是,调参优化难度高,需要大量的数据和成熟的调度作为参考。同时,针对于不同的优化目标,可以通过改变参数、改变变量的方式,使方程最快适应此目标。

第一次架构中,共有7个类,其中有一个主类,三个线程类,三个结构/功能类。

第二次作业的架构中,相对于第一次架构做了以下改变:

第三次作业在前两次作业的基础上,进行了以下改变:

通过主类构建并启动输入线程、调度线程和电梯线程。
输入线程和调度线程、电梯线程共享主请求队列,输入线程和电梯线程充当生产者,标准输入的请求和reset时释放的请求被加入到主请求队列中,又被调度线程分配到各个电梯中去。分配的过程借助了调度线程和电梯线程间共享的处理队列实现。
重置请求则是通过输入线程直接传递给对应的电梯,在接收到重置请求时,输入线程会将和对应电梯共享的ResetReq置于待处理的高位,并将需要重置的内容传递进去。
双轿厢设计的核心在于两轿厢不能同时进入换乘楼层。
为解决这个问题,可以采用信号量来实现,即将对换乘楼层的访问设置为临界区,只有取得信号量的轿厢才能进入。
当然,还有一种更为简单的方法——“划界而治”法,即在接收到第二类重置请求时,将楼层划分为两个交集为空的区间,两个轿厢分别运行在这两段区域内。采用这一做法,可以在根本上避免同时进入换乘楼层的现象。
具体的实现方法为,从换乘楼层进行切割,若换乘楼层与电梯序号奇偶性相同,则换乘楼层归属上区;否则归属下区。采用这种方法,可以避免6部电梯同时resetⅡ且换乘楼层相同时造成的“断流”现象。
在本单元的作业中,出现过两种多线程设计中的经典问题——CTLE和死锁。
CTLE现象的出现通常伴随着轮询,常常是因为某一进程没有合理的休眠,而在while循环中不断要求访问某一资源造成的。
为了排查这个问题,采用了print大法进行调试。在每个进程循环处打印不同的信息,通过观察输出,锁定出现轮询的线程;继续利用print输出,逐步缩小范围直至出现问题的分支;最后结合逻辑推演与判断,找到问题出现的具体原因,并加以改正。
死锁通常是由于不恰当的资源获取顺序造成的,通常的表现形式为程序运行不结束,此时如果利用IDEA提供的线程转储功能查看运行状态,会发现有(至少)两个线程都在等待获取对方已经占有的资源。
为解决这一个问题,可以限制资源的获取顺序,如限定在获取资源时,采用从全局到局部的方式,即强制线程按照主请求表--电梯等待队列--reset信号的顺序获取资源,以预防死锁的出现。同时,对于加synchronsized的方法,调用时要万分小心(因为调用此方法也会要求锁)。
在本单元的三次作业中,我深刻体会到静态分析在解决多线程电梯问题中的关键性;通过不断的实践,也对线程安全和层次化设计有了更为深入的理解。
线程安全是我在本次作业中重点关注的另一个方面。多线程环境中,资源的共享和竞争是常态,因此线程安全显得尤为重要。我通过仔细设计同步机制,如使用锁(Lock)来确保线程间的正确交互和数据一致性。同时,我也注意到了避免死锁和活锁的重要性,通过合理的锁粒度选择和锁的获取顺序来预防这些问题。
层次化设计在提升代码可读性和可维护性方面发挥了重要作用。我尝试将电梯系统的功能与线程进行分离,划分为不同的层次,每个层次负责特定的任务,并通过清晰的接口与其他层次进行交互。
总的来说,通过本单元的三次作业,我深刻认识到了线程安全和层次化设计在解决多线程电梯问题中的重要性。这些心得体会不仅对我当前的学习有所帮助,也将对我未来的编程实践产生深远的影响。