309
社区成员
发帖
与我相关
我的任务
分享哇塞~最负盛名,传言中最最恐怖的多线程电梯终于结束啦!庆祝!
随着作业不断迭代,(在与ai的深入交流下)我们对同步块逐渐有了深入的认识,从一开始想要偷懒“无脑加锁”,发展为为了追求更高性能分而不断拆分更细的粒度锁。
1. 锁的选择与分布
全局等待队列锁 (waitQueue): 借鉴于实验课中的代码,waitQueue类在我的代码中实例化的结果就是GlobalWaitQueue(全局队列),Dispatcher 线程在此处等待,Input 线程在此处notifyALL(),是经典的生产者-消费者架构。
电梯内部状态锁 (stateLock): 保护单部电梯的状态,如当前楼层,运行方向,门的状态。
换乘层专属共享锁 (f2Lock - ReentrantLock): 第三次作业的核心(在ai的推荐下我使用了ReentrantLock,它类似一个只处理“写”任务的读写锁)。主副轿厢竞争同一把物理层面的“碰撞锁”,只有抢到锁的人才能到达F2。为了保证绝对安全且不死锁,我将它独立出来,只在跨越特定楼层边界时起效。
1. 调度器与线程的交互模式
在讨论课上我们知道:由于大家都高度借鉴了实验课上的代码,所以Dispatcher与其他类的关系基本相同:独立于所有 ElevatorThread(四肢)运行。不同之处主要集中在dispatch的逻辑上(也就是性能分的关键)。
2. 调度策略:影子电梯的成本推演
关于调度策略,笔者一开始毫无头绪:想要通过列举单纯的if-else选出“最优”的电梯,考虑的情况过多过于复杂,也不利于实现。在ai的建议下,我引入了 ElevatorSimulator(影子电梯)(据说是老学长的亲传)。 每次派单前,调度器会对当前活着的电梯进行快照克隆(Snapshot),让影子电梯在沙盒里跑一遍 DFS/启发式推演,算出能最快将电梯内的所有人(包括新加入的目标乘客)全部送到位的电梯。
之所以选择影子电梯,是因为他的实现令人瞠目结舌的简单!直接把原有的电梯运行逻辑复制过来,删掉(开关门间隔,楼层移动)等待时间就搞定了。唯一的不足在于电梯状态快照的传递,一不小心就容易出错。由于在最早的电梯运行逻辑里我们就已经使用了LOOK算法,所以沙盒模拟相当“智能”。
3. 引入双轿厢后的dispatch逻辑
双轿厢的特性导致部分乘客的请求需要换乘处理,无论是对于乘客本身而言(被中途踢下去要多等一趟电梯),还是对于我们电梯性能而言(换乘可能导致电梯门打开次数增加),换乘都是极为麻烦的情况,所以在计算最优电梯时,我们要为需要换乘的情况多加一个足够大的值,从而实现“能不换乘尽量不换”。
多线程的 Bug 往往不是明面上的“逻辑错误”,而是“想当然后”不幸被忽略的边界情况……我在最后一次迭代中不幸犯了这样的错误,导致被人hack了16次,真崩溃:
Bug 1:F2 换乘层的“撞车”
症状: 主副轿厢同时出现在 F2。
分析: 我最开始的思路是,假设轿厢a正在F3前往F2,而此时轿厢b刚好位于F2,那么a到达F2所需的时间为:等待b离开F2(0.4s)+a进入F2(0.4s)=0.8s,如果两台电梯能紧紧挨着向下:ab同时往下走一层,那么a到达F2所需的时间就仅需0.4s,这样就省出了宝贵的0.4s!为了这0.4s,我对处在F2楼层的轿厢运行设计为:先释放楼层锁(释放完毕后,a就能拿到锁,就能向下运行),再运行……然后不幸被评测机杀死了,原因是评测机检测到b还没输出到达F1的信息,a就输出了到达F2的信息(多线程之美啊),直接判定为两台电梯叠加到了F2(惨烈)。
debug 与修复: 唉,实际上就是改一下释放时间的事,先移动再放锁,不省那0.4s,就好了。
Bug 2:派单给“死尸”(派给 SHUT 状态电梯)
症状: Elevator 9 is in State.SHUT state, so it can not receive any passenger。
分析: 在处理 UPDATE/RECYCLE 时,我的调度器跑得比评测机还快。副轿厢刚在内存里被激活,还没来得及打印 UPDATE-END 向评测机报备,调度器就把人塞进去了。这就造成了“把人派给未出生/已死亡电梯”的格式错误。
修复:电梯升级时,我应当先输出已经成功UPDATE的信号,再调整布尔值使dispatcher能向电梯中塞人,我敲代码时没注意这一点,导致出现了一个细微的时间差:UPDATE信号还没输出,dispatcher就检测到新电梯的布尔值合法,开始往里塞人了,导致输出错误。
线程安全绝不是单纯的“加锁”,而是“状态机的安全管理”;
好的架构设计极大有利于保护线程安全。
层次化设计的护城河: 模仿实验课的设计,我的系统被严格划分为:数据缓冲层(WaitQueue)、大脑控制层(Dispatcher + Simulator)、机械执行层(ElevatorThread)。 这种设计带来的巨大红利在第七次作业体现得淋漓尽致。当加入双轿厢时,我的dispatcher基本不需要改动,只需要让“模拟器”适配新的上下限;电梯流中在 moveOneFloor 里加上 F2 的物理碰撞检测,就完活了
。模块之间低耦合,使得我能在一个局部动刀,而不至于引发全局的死锁雪崩。
对线程安全的重新定义: 起初,我认为线程安全就是防止几个线程把变量写脏。后来我才明白,控制状态的转换真空期才是核心。 比如,在双轿厢把人踢出(OUT)再由 Dispatcher 重新分派(RECEIVE)的过程中,存在长达几百毫秒的“人不在车上、也不在队列里”的真空期。如果系统架构没有全局视角(未完成请求计数器),很容易在这个真空期内发生误判而提前自杀。
到了这个话题咱们可谓非常有发言权,如果说unit1的作业是因为代码能力实在没入门而导致迫不得已依赖ai,那么到了这一单元,ai就已经从“无情的作业输出机器”变成了提供高级查询的CSDN。这次在充分借鉴了实验课代码的基础上,我成功搭建出了各个线程间的基本关系,ai给我提供的帮助主要在于算法以及优化方面:在不知道改如何实现某一功能(如dispatch逻辑与主副轿厢结构设计)时,我会求助于gemini,它居然会告诉我“老学长通常会用……方法(不知道是不是它瞎编的)”;另外某些算法实现部分(如DFS)交给ai来写也是十分方便。同时,我还让ai帮我生成了评测机。
有了ai辅助锁的设计、评测机的搭建,多线程的令人头秃率确实大大下降了。但是ai的辅助确实也存在消极的一面,我确实觉得最后自己对锁的理解还是不够深入,同时,虽然已经尽力把ai放在辅助的位置上,但是一不留心还是会被ai牵着鼻子走,等到反应过来之后才发现代码里已经有不少地方被ai改的面目全非,“看不懂了”。
希望课程组将来优化作业任务的设计:多线程电梯的实现对ai而言可能确实“太简单了”。