OO-Unit2
Unit2博客作业
同步块的设置和锁的选择
在电梯系统的多线程实现中,锁和同步块是非常关键的部分。为了保证线程安全,避免数据竞争和资源冲突,我使用了多种锁的机制来管理共享资源。
- 锁的使用:
ReentrantReadWriteLock被用于RequestQueue和RequestTable类中,这使得读操作可以并发执行,而写操作会加锁,保证了在写入数据时不会发生竞争。 - 同步块的设置:在
ElevatorThread类中,通过使用synchronized关键字对一些关键方法进行同步处理,保证了线程在对电梯状态进行修改时,不会发生资源争夺。例如,move()方法使用了synchronized(lock)来防止在双轿厢模式下两部电梯同时进入 F2 层导致冲突。 - 锁与同步块的关系:锁与同步块的关系表现在它们在多线程操作中的作用。锁控制对共享资源的访问,保证在同一时刻只有一个线程能访问共享数据,而同步块则是执行具体线程操作时加锁的地方。通过合理的锁机制,电梯系统能够在多线程环境下保持一致性和正确性。
调度器的设计
调度器与线程的交互
调度器 (Schedule) 的任务是管理乘客请求并调度电梯线程执行相应的任务。调度器与电梯线程通过共享资源(如 RequestTable, RequestQueue, Shaft)进行交互。
共享资源与线程同步:多个电梯线程和调度器线程需要共享和更新请求数据,所有涉及请求的修改都需要加锁以防止并发冲突。使用
ReentrantReadWriteLock来保护数据,确保在一个线程读取共享资源时,其他线程能够安全地访问。对于写操作,线程会被阻塞直到锁释放。调度器唤醒电梯线程:当调度器检测到有新乘客请求时,它会将请求分配给电梯并通过
elevator.wakeUp()方法唤醒电梯线程,让电梯开始执行任务。电梯线程在完成某些任务后可能需要等待新的指令,调度器通过notifyAll()和wait()的机制来协调线程之间的等待与执行。调度策略
调度策略的核心目标是合理分配乘客请求给合适的电梯,尽量减少电梯的运行时间和能耗。
策略实现:在
Strategy类中,根据电梯的当前状态(如ShaftMode),电梯是否能接收新请求以及电梯是否需要进行检修或改造等情况,决定电梯的行为。电梯行为包括:是否移动、是否开门(这一点可以做一个小优化,一直到移动或者结束前再关门,这样可以避免一些不必要的开关门重复)、是否等待等。调度策略:调度策略
costFunc通过检查ElevatorThread当前的负载、目标方向、当前请求的楼层等信息来判断最合适的电梯。比如,若电梯未满载且在合适的方向,系统会优先让电梯继续运行以接载更多乘客。策略与任务优先级:调度策略
costFunc优先处理顺路的请求,避免不必要的回头。系统会计算电梯的“成本cost”,例如路径惩罚、忙碌惩罚和负载惩罚,来决定哪部电梯最适合接载当前乘客。策略对指标的适应性
调度策略不仅要考虑电梯的运行时间,还需要适应多个性能指标(如能耗、完成时间等)。这一点主要通过
CostFunc来实现。- 运行时间:调度策略会优先选择最短路径和最少停靠的电梯,以减少总运行时间。在双轿厢模式下,电梯可以并行工作,减少整体的运行时间。
- 能量消耗:电梯的能量消耗与开关门次数、移动层数密切相关。调度策略通过选择顺路接载的乘客,减少不必要的电梯移动,并通过最小化停靠次数来降低能耗。
- 乘客完成时间:调度策略通过最优分配电梯,确保每个乘客在最短时间内到达目的地,同时避免电梯在无乘客时空驶。电梯空载时,系统会尝试选择最优路径,减少不必要的行程。
出现过的bug以及debug方法
出现过的bug
第一次作业
强测与互测均无bug,但是本地测试的过程及其之痛苦,总共提交了11次hh
第二次作业
强测无bug,互测2个bug
- 在6个电梯同时进入维修状态时,仍然会正常进行
receive
原因:根据电梯的cost给电梯派活,如果电梯此时不能派活,就把cost赋值为MAX_VALUE;找到电梯cost的最小值,如果有相同的,就随机选一个;这样的机制无法应对全部cost均为MAX_VALUE的情况。 - 在5个电梯进入维修状态,1个电梯可以用时,突然派发大量请求,则全部派给了这个电梯,电梯过载超时。
原因:没有对电梯的载重进行限制,可能会导致电梯在极短时间内收到极多请求,而其它电梯回归正常状态后也无法参与工作。
第三次作业
强测和互测
- WRONG_ANSWER:repair、recycle、update在输出begin的同时输出回流请求的receive信息,即使时间戳同时但是上下顺序不对,造成评测机判误。实际上先在
shaft里输出begin再在elevatorThread里clearQueue就可以了。这个是随机触发的bug,本地高概率再现。 - REALTIME_EXCEED:线程没有安全结束,在某些条件可能会不稳定随机触发TLE,事实上我最终也没能明白是怎么回事,一样的代码交了两遍第二次交就全过了。
简单记一下修改过的地方:- 没有将
elevatorThread里的curFloor的get方法加上同步,在Shaft里endMaint/Repair/Recylce之后除了notifyAll()唤醒shaft,也要唤醒主副电梯wakeUp,保证线程的安全结束。 - 在
Rec_Accept状态下也保证互斥(原来只保证double状态下互斥)。
- 没有将
面对多线程程序的debug方法
从复杂的综合测试数据中提炼出导致bug的点,再根据点定位相关代码。
绝大部分原因都出在ElevatorThread或者Schedule上。
还有就是程序EndFlag的设置,保证程序的正常结束,别的倒是很少出问题。
对线程安全和层次化设计的理解
从多线程程序的角度来看,线程安全和层次化设计是至关重要的。在本单元作业中,我的电梯系统采用了明确的层次化设计,每一部分都有明确的责任和操作流程:
- 线程安全:通过使用合适的锁机制和同步块,保证了多线程环境下的线程安全。例如,
RequestTable使用了读写锁,确保多个电梯线程可以并发读取请求,而不会干扰写入操作。 - 层次化设计:系统的设计遵循了清晰的层次结构。每个电梯线程负责单独的电梯操作,调度器负责整个系统的调度和资源管理,
RequestTable和RequestQueue等数据结构负责共享数据的管理。这种层次化的设计使得系统具备较好的扩展性和可维护性。
大模型的使用心得
模型名称:gpt5.4
使用
- 查找具体的用法:比如读写锁具体的使用。
- 思路推荐:比如第三次作业中
Shaft类的设计。 - 代码走查:开始测试前先静态分析代码;测试发现问题后精准定位可能哪里除了问题。
- 测试数据的生成:给它一些极端数据的测试思路和方向,可以生成完整的测试数据。
优势
- 查找用法上确实很好用,还会举出简单易懂的例子,很快可以学会用。
- 代码走查可以发现较大的核心问题,避免测试时再定位。
- 思路推荐上可以在设计上给一些自己想不到的灵感。
- 可以按照强弱要求生成不同的测试数据,方便测试。
- 可以检查输出的正确性,大大提升测试效率。
困难
- 在生成测试数据上,一般必须给出数据设计思路,不然生成的数据点都太过薄弱。
- 在代码走查上,很容易给出错误或者无用的修改建议,需要自己仔细甄别。
- 在思路推荐上,一般可以采用大体的方向和思路,但是不能仔细聊,越聊越偏,很容易就不知道跑哪去了。
感受
- 可以在遇到困难的时候找大模型聊一聊,但不能完全信任它。有时候很有帮助,有时候会一直在原地徘徊,大模型回答了跟没回答一样,这时候不如求助聪明优秀的室友,可能稍微点拨一下就突然明白了,比和大模型瞎耗有用。
- 不能过多使用大模型进行思路上的全局调整,这样会让自己对自己的代码很陌生,大大提高debug所需要的时间和精力。
- 大模型更多有一种面向问题解决问题的感觉,出现了bug再解决,这样会使自己的代码多出了很多感觉不属于自己的部分,很难受。
- 大模型在架构和整体思路设计上感觉不太可靠,至少这一点上我还是坚持手搓。先把架构搭起来,再把主要的问题比如如何实现互斥如何解决多并发如何实现流程模拟等等想明白,最后在具体实现上遇到困难时再配合大模型去完成。这样就保证了自己能够熟悉自己的代码,遇到问题也可以很快解决,而不是和大模型兜圈子,感觉很耗心力。
真实体验和感受
- Unit2感觉上比Unit1花了我更多的时间和精力。Unit1感觉基本上一天就可以跑通,再花半天测试,花半天做最终优化(实际上可以优化的点也非常少)。而Unit2,我的天哪,基本上写可能就要一天半,debug又是一天半,然后优化也是有种优化的无关痛痒的感觉。大部分时候都充满了疑问为什么呢,为什么呢,为什么呢。
- Unit2的话感觉直接提升了我的大模型的使用能力,让我能够更有效地利用这个工具来辅助自己完成作业。
- 还有一点心得就是可以公测过了之后第二天再花点时间看一看,做一做最终的优化和一些小调整,这样可以无意中解决一些bug,而且第二天的话也从那种想死的心境中出来了,可以更冷静客观地观察自己的代码。(事实上我前两次作业是这么做的,第三次作业周末玩嗨了,结果愉快的进C房了)
- 挺好的没什么建议,反正多线程注定是要痛苦一波的。不过个人感觉今年的迭代作业设置比往年更合理更有迹可循(?可能),然后就是强测实在感觉不是很强。


