307
社区成员
发帖
与我相关
我的任务
分享我的并发结构核心其实比较稳定:InputThread 负责读入请求,Dispatcher 负责统一分配,Elevator 作为执行线程运行,RequestQueue 负责线程间传递任务,到第七次作业再增加一个 ShaftCoordinator 处理双轿厢同井道协同。同步块的设置基本就是围绕这几个共享对象展开的。
RequestQueue.java 里几乎所有访问等待队列的方法都加了 synchronized,因为它本身就是典型的生产者-消费者缓冲区:Dispatcher 会往里放乘客,Elevator 会从里取乘客,还要支持 awaitChange() 这类等待唤醒。这里锁保护的不只是 waitingPassengers 这个容器,还包括 version 和 end 这两个控制线程协作的状态,所以像 addPassenger()、pickupAtFloor()、drainAllPassengers()、setEnd() 都必须放在同一把锁下,否则就会出现“队列状态变了但版本号没同步更新”或者“线程永远等不到唤醒”的问题。
Dispatcher.java里的锁则更多是保护“全局调度状态”,例如 pendingPassengers、unfinishedPassengers、unfinishedControlTasks、各电梯的 active/receiveEnabled/lowFloor/highFloor。这些状态是调度正确性的核心,因为第七次作业里一个乘客能不能被分给某部电梯,已经不只是“这部电梯忙不忙”,而是和它当前是不是主轿厢、是不是备用轿厢、是不是正在检修/改造/回收、当前可达楼层范围是不是已经变化都有关。所以我把 addPassenger()、recyclePassenger()、completePassenger()、addMaintainTask()、addUpdateTask()、addRecycleTask()、onUpdateEnd()、onRecycleEnd() 这些都做成同步方法,本质上就是保证“调度视图”在一个时刻是一致的。这里我最大的体会是:锁的意义不是单纯防止容器并发修改,而是保证一组状态一起变化时对别的线程可见。例如 onUpdateEnd() 里面同时恢复 active、receiveEnabled、lowFloor/highFloor,如果这些更新不在同一个同步语义里,调度器就可能在某个中间态把乘客错误分给一部范围还没完全恢复的电梯。
到了第七次作业,ShaftCoordinator.java变成了一个新的同步重点。双轿厢同井道的问题本质上是“两个轿厢对井道状态的理解必须一致”。所以这里我把 doubleMode、primaryFloor、spareFloor、primaryTarget、spareTarget、recycleEnding 都放到一个同步对象里统一维护。尤其是后面我修 bug 时非常能说明锁和状态的关系:一开始只检查对方“当前楼层”会导致两个轿厢同时向 F2 移动,后来加上 primaryTarget/spareTarget 做“目标预约”;再后来发现回收结束时主轿厢 F2 无 RECEIVE 让位移动和 RECYCLE-END 会撞时间,又加上 recycleEnding 和 prepareRecycleEnd() 形成一个回收结束屏障。这个过程让我很明确地意识到,多线程里真正难的不是“加没加锁”,而是“锁到底保护了哪个时序不变量”。
三次作业里我的调度器一直都是 Dispatcher.java 这一个类,它既接收 InputThread 投喂进来的原始请求,也负责把乘客重新分发给各个 Elevator 的 RequestQueue。InputThread 只生产输入,Dispatcher 消费输入并生产“已 RECEIVE 的任务”,每个 Elevator 再去消费属于自己的任务。到了后两次作业,中转乘客和检修/改造/回收造成的“任务回流”也还是回到 Dispatcher,所以它既是调度中心,也是全局状态中心。
这种设计在前两次作业里已经够用了,到第七次作业以后它的价值更明显。因为双轿厢模式下,分配乘客时已经不能只看“最近的电梯”,而必须同时看这部电梯当前是单轿厢还是双轿厢、主轿厢还是备用轿厢、可达区间是不是 B4-F2 或 F2-F7,以及控制请求是否正在影响它的可用性。所以我在 Dispatcher 里维护了 active、receiveEnabled、lowFloor、highFloor 这几组数组,然后通过 supportsPassenger() 先过滤掉不该接这个人的电梯,再在 selectElevator() 里打分。这个打分从早期简单的“队列长度 + 距离”逐步调整为现在的“队列长度 + estimatePickupCost”,其中 estimatePickupCost() 在 Elevator.java 里会结合当前楼层、运行方向、载客数做估价。这个变化其实就是我在调度策略上的一个核心改进:不再只看谁离得近,而是尽量把乘客分配更顺路、代价更的电梯。
互测时,有个样例很疯狂,hack到了很多人(包括我)。先让五部电梯进入维修状态,维修期间输入大量乘客请求,由于电梯的乘客队列没有设置上限,所有乘客被分配到唯一一个没有进行维修的电梯,从而导致了TLE。
这个bug的本质是电梯资源没有得到有效的利用,电梯检修完后无乘客分配,乘客全分配到了一个电梯。我参考了水群里的建议,给电梯的乘客请求队列RequestQueue加了一个MAX_SIZE,从而避免把所有乘客全分配到一个电梯。
第三次作业的互测中,有一个样例会让我的elevator在无recevie的情况移动。双轿厢时,主轿厢 F2 无 RECEIVE 让位移动和 RECYCLE-END 会撞时间,我考虑到了这个问题(在水群中看到的),所以我加上 recycleEnding 和 prepareRecycleEnd()这两个方法,主轿厢移动后才输出recycle-end。但hack我的数据正好可以让recycle-end和主轿厢arrive同时间戳,从而WA。
我的解决办法是在输出recycle-end前先sleep(1),从而使这两个输出的时间戳不同。
我每次写完代码后,都会先让AI帮我分析一下,看看哪里可能有问题,如果有WA,我会先在水群里看看有没有人讨论(第二次作业就是这样debug)。AI分析+代码走查可以解决大部分bug,我也尝试过用AI搭建一个评测机,但效果不尽人意。
做完二单元以后,我觉得线程安全绝对不是“哪里不放心就加个 synchronized”,而是先明确系统里的共享状态有哪些,再明确这些状态之间应该满足什么关系。比如在我的代码里,RequestQueue 关心的是“队列内容、结束标志和版本号一致”;Dispatcher 关心的是“电梯可分配状态和全局待分配状态一致”;ShaftCoordinator 关心的是“井道模式、当前楼层、预约目标和回收结束屏障一致”。每一层只维护自己这一层的不变量,这就是层次化设计在并发程序里的意义。如果把所有逻辑都揉在 Elevator 里,不仅会写得很乱,而且出了 bug 根本分不清到底是调度错了、执行错了,还是协同错了。我这次能比较持续地往下修,也是因为结构一直是分层的:输入层只管读入,调度层只管分配,执行层只管跑,井道协调层只管双轿厢互斥和回收边界。这个层次感让我越来越相信,好的架构不是为了“好看”,而是在 bug 和需求同时变复杂时,仍然能让你知道应该去改哪一层。
本单元我主要使用GPT5.4和豆包,一开始对多线程不太熟悉时,通过豆包可以比较方便获得一些知识;设计架构,优化分配策略(评分策略的优化),debug,写评测机我主要使用GPT5.4.我感觉现在的大模型太强了,有一种危机感。
至于二单元本身的体验,我的真实感受是:很折磨,但收获很大。前期觉得困难主要在 wait/notify 的写法,后面才发现真正难的是:如何维护共享状态的一致性,如何让状态切换严格满足规则。尤其第七次作业,把检修、双轿厢改造、双轿厢回收、F2 换乘、RECEIVE 约束全叠在一起之后,很多 bug 已经不是“功能没实现”,而是“并发语义稍微差一点就错”。这对我的打击挺大,但反过来也逼着我真正去理解线程安全和层次化设计。
总的来说,二单元让我最深的体会就是:并发程序最难的不是让多个线程跑起来,而是让它们在所有边界交错下都跑对;层次化设计最大的价值也不是形式好看,而是在要求变复杂、bug 变多的时候,仍然能让你知道应该从哪一层下手。