2024OO第二单元反思与总结

蒋孝淳-22373369 学生 2024-04-20 15:40:45

同步块与锁

同步块与锁的选择,关键在于确定会被线程共享的资源有哪些。在确定共享资源之后,要根据被共享资源的访问特性(读者/写者的数目),选择合适的“锁”。

  • 第一次作业:第一次作业中,采用了“生产者-消费者”模式,调度线程与输入线程、电梯线程之间各有一个请求表RequestTable对象作为共享资源。这两个对象的读者和写者均各有一个,因此只需要保证“写时不读”,采用对读-写方法加同步锁synchronsized进行保护。
  • 第二次作业:新增Reset请求,电梯与输入线程之间共享一个(1读1写)的信号resetSign,采用在读-写方法加同步锁synchronsized进行保护;同时,reset请求到来时,电梯将内部乘客和等待队列里的乘客写回主请求列表,主请求列表变为1读多写资源,可沿用第一次作业中的保护方法。
  • 第三次作业:本次作业没有新增需要共享的资源,因此沿用第二次即可。

临界区的充分小

为了实现临界区的充分小,采用以下的几种策略:

  1. 封装原子操作:即加synchronsized的方法,保证其内部仅有简单的原子操作,如加入队列/返回队首元素等。
  2. 对于复杂的行为,应该对其功能进行明确的规定,并仅仅在可能被其他进程打断且会造成结果不可预测的代码块增加同步锁,这样可以保证临界区的尽可能小。
  3. 减少无关语句:通过调整代码的组织结构,使得临界区内的代码尽量和临界资源相关,减少在临界区内运行非竞争性语句,造成性能的浪费。

调度器设计

类支撑

建立了调度线程,通过与输入线程共享主请求队列获取请求,并执行对应的策略,将他们放入与电梯线程共享的等待队列中。

在编写代码的过程中,尤其需要注意的是线程的终止条件。

  • 第一次作业中,因为不涉及到reset请求,故所有的输入完成后,可以将主请求队列的isEnd置高位;主请求队列为空时,将各电梯的等待队列isEnd置高,并结束调度线程。
  • 第二/三次作业中,由于新增了reset请求,电梯同时充当请求的生产者,此时无法把Input的结束以及主请求队列的完全分配作为结束信号。为解决这个问题,采用计数的策略,新增一个计数器,每当input中新增一个乘客请求,计数器加一;当乘客从请求的楼层下电梯时,计数器减一,利用input结束 && 计数器为0 && 没有待处理的reset请求作为结束信号。

调度策略

  • 第一次作业:直接分配。这次作业中,每个乘客的请求中给定了需要搭乘的电梯,只需要按要求将其分配到对应的电梯即可。

  • 第二/三次作业:公式法(调参法)。其核心思想在于通过电梯运行时的状态参数(如当前所在楼层与目的楼层的距离、运行速度、载客量、等待队列的长度等),建立合适的评估函数,对几个电梯进行评估,最终选择评分最高的电梯。
    这种方法实现较为简单,效果取决于评估函数,以及测试数据的覆盖率/强度,但从极限的考虑来说,一个真正好的函数应该可以超过影子电梯达到调度的真正极限。
    具体来说,函数的建立分为两个阶段:确定函数形式与确定参数取值。第一,函数的形式上,一般不选择线性函数(这与实际相关,即考虑边际效益递减),但函数形式依然复杂多样,需要更多次的尝试。第二,参数取值的调整,需要借助工具,多多调整。这一步需要大量的数据作为支撑,否则可能造成“过拟合”的问题。
    调参法最大的难点在于,需要一个较为成熟的调度器作为比较指标,以及需要大量数据的自动测试。当然,如果有可能的话,可以尝试利用机器学习的方法来解决调参的问题(研讨课上小组成员所提到)。
    最后,评估函数不适宜过于复杂,过于复杂的函数往往难以调试,且容易造成针对特定数据的优化(大坑)。力推写一个好一点的自动调参机。

  • 被ban掉的绝妙实现:自由竞争。这种方法借助了自然界中“物竞天择,适者生存”的思想,每当有乘客提出请求时,所有空闲(或顺路)的电梯同时向请求的楼层运行,到达最早的电梯将接到这名乘客。这种方法实现简单,但是由于本年度OO课程新增的RECEIVE请求,以及性能分对耗电量的限制,导致这一经典方法被ban掉了。

方法评价

调参是世界上最好的方法
调参实现难度极低,同时任意一个较为合理的函数都能达到较好的效果,投资-回报比高。但是,调参优化难度高,需要大量的数据和成熟的调度作为参考。同时,针对于不同的优化目标,可以通过改变参数、改变变量的方式,使方程最快适应此目标。

线程协同--架构分析

架构迭代与UML类图

img

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

  • 线程类:输入线程、调度线程和电梯线程。输入线程用于读入和解析标准输入,并将主请求放入主请求队列,与调度线程共享;调度线程采用特定的调度策略,将主请求队列中的乘客请求分配给各个电梯;电梯线程用来控制电梯的行为,作为一种“行为”而存在,与电梯类不同。
  • 结构/数据类:请求Request类,用来盛放一个请求;请求队列RequestTable,重新实现了队列的常用方法,以确保其线程安全;电梯类Elevator,存储了电梯的状态,容量,内部乘客等信息,是电梯本身的数据与结构的聚合,并不具有行为的概念,与电梯线程不同。

img

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

  • 新增TestMain类,用于程序调试,可以按照时间戳投喂数据。
  • 新增ResetReq类,用来存放重置请求,其实例化对象通过在输入线程和电梯线程内共享,实现重置请求的传递。

img

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

  • 新增Signal类,其内部维护着一个“信号量”,用来存储输入请求以及完成请求的数量,作为线程结束的判断标准而存在,保证其内部方法均线程安全。

类协作关系

img

通过主类构建并启动输入线程、调度线程和电梯线程。
输入线程和调度线程、电梯线程共享主请求队列,输入线程和电梯线程充当生产者,标准输入的请求和reset时释放的请求被加入到主请求队列中,又被调度线程分配到各个电梯中去。分配的过程借助了调度线程和电梯线程间共享的处理队列实现。
重置请求则是通过输入线程直接传递给对应的电梯,在接收到重置请求时,输入线程会将和对应电梯共享的ResetReq置于待处理的高位,并将需要重置的内容传递进去。

迭代中的变与不变

发生改变的类

  • Elevator && ElevatorThread:第二三次作业分别支持实现两类重置,实现了新的方法使其支持新增的功能。
  • Assignment:修改调度策略,调参,改动较多。
  • InputThread:每次输入格式有所不同,需要对输入线程进行改变;新增Reset请求后,需要与ElevatorThread共享重置信号,需要对功能上进行拓展。
  • ResetReq类:重置请求,第二次作业中只支持第一类重置,第三次作业中拓展了其功能,使其能够支持第二类重置。

没有改变的类

  • Request类:存储乘客请求,提供乘客信息的输出功能。
  • RequstTable类:封装过的队列,保证线程安全,存储一列Request对象。
  • TestMain类:进行数据投喂。
  • Signal类:作为判别线程结束的标志。
  • MainClass类:程序的入口,启动各个线程。

双轿厢设计

双轿厢设计的核心在于两轿厢不能同时进入换乘楼层。

为解决这个问题,可以采用信号量来实现,即将对换乘楼层的访问设置为临界区,只有取得信号量的轿厢才能进入。

当然,还有一种更为简单的方法——“划界而治”法,即在接收到第二类重置请求时,将楼层划分为两个交集为空的区间,两个轿厢分别运行在这两段区域内。采用这一做法,可以在根本上避免同时进入换乘楼层的现象。

具体的实现方法为,从换乘楼层进行切割,若换乘楼层与电梯序号奇偶性相同,则换乘楼层归属上区;否则归属下区。采用这种方法,可以避免6部电梯同时resetⅡ且换乘楼层相同时造成的“断流”现象。

bug && debug

在本单元的作业中,出现过两种多线程设计中的经典问题——CTLE和死锁。

CTLE

CTLE现象的出现通常伴随着轮询,常常是因为某一进程没有合理的休眠,而在while循环中不断要求访问某一资源造成的。

为了排查这个问题,采用了print大法进行调试。在每个进程循环处打印不同的信息,通过观察输出,锁定出现轮询的线程;继续利用print输出,逐步缩小范围直至出现问题的分支;最后结合逻辑推演与判断,找到问题出现的具体原因,并加以改正。

死锁

死锁通常是由于不恰当的资源获取顺序造成的,通常的表现形式为程序运行不结束,此时如果利用IDEA提供的线程转储功能查看运行状态,会发现有(至少)两个线程都在等待获取对方已经占有的资源。

为解决这一个问题,可以限制资源的获取顺序,如限定在获取资源时,采用从全局到局部的方式,即强制线程按照主请求表--电梯等待队列--reset信号的顺序获取资源,以预防死锁的出现。同时,对于加synchronsized的方法,调用时要万分小心(因为调用此方法也会要求锁)。

心得体会

在本单元的三次作业中,我深刻体会到静态分析在解决多线程电梯问题中的关键性;通过不断的实践,也对线程安全和层次化设计有了更为深入的理解。

线程安全是我在本次作业中重点关注的另一个方面。多线程环境中,资源的共享和竞争是常态,因此线程安全显得尤为重要。我通过仔细设计同步机制,如使用锁(Lock)来确保线程间的正确交互和数据一致性。同时,我也注意到了避免死锁和活锁的重要性,通过合理的锁粒度选择和锁的获取顺序来预防这些问题。

层次化设计在提升代码可读性和可维护性方面发挥了重要作用。我尝试将电梯系统的功能与线程进行分离,划分为不同的层次,每个层次负责特定的任务,并通过清晰的接口与其他层次进行交互。

总的来说,通过本单元的三次作业,我深刻认识到了线程安全和层次化设计在解决多线程电梯问题中的重要性。这些心得体会不仅对我当前的学习有所帮助,也将对我未来的编程实践产生深远的影响。

...全文
65 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧