304
社区成员
发帖
与我相关
我的任务
分享我这三次作业里一直没有换锁模型,基本都在用 synchronized。原因不是它有多高级,而是它足够直接,和 wait/notifyAll 也比较好配合。二单元最怕的不是“锁不够高级”,而是共享状态到底归谁管不清楚。
第一次作业的时候,共享内容还比较少,主要就是请求集合和结束标志。那个阶段我对同步的理解比较朴素:输入线程会放请求,电梯线程会取请求,所以这些状态必须保护起来,不然很容易出现有人一直在等、有人已经把数据改掉了、但别的线程还不知道的情况。
第二次作业开始我对锁的理解比前面清楚一些了。全局调度相关的东西,我放在调度中心管;单部电梯自己要处理的等待乘客和轿厢状态,我尽量让电梯线程自己管。这样做之后,锁的职责会比较明确:全局锁负责分配和结算,局部锁负责单梯内部状态,不会互相乱穿。
第三次作业真正让我意识到“锁要跟着共享资源走”。双轿厢模式下,主轿厢和副轿厢在 F2 会发生竞争,这个时候问题已经不是某一部电梯自己的状态了,而是同井道共享资源的问题。所以我后来单独放了一个 ShaftController 去管这件事,而不是继续把逻辑塞回某个电梯线程里。
我现在对同步块和其中语句的理解是:同步块里应该放“检查条件、修改共享状态、唤醒别的线程”这一类真正需要原子性的内容;像电梯移动、开关门等待、测试运行这种耗时动作,就不要长时间占着锁做。否则程序虽然表面上安全了,但并发性会很差,而且很容易把本来分得开的线程又串回去。
如果只看结构,我这套程序其实很简单,就是输入线程、调度中心和多部电梯线程。
第一次作业里因为乘客已经指定了电梯,所以严格来说全局调度器还不算复杂,重点主要在单部电梯内部怎么决定下一步动作。那个时候更像是在写“单梯策略”。
第二次作业开始,调度器才变成系统核心。输入线程持续读请求,调度中心决定 RECEIVE 给谁,电梯线程执行具体动作,执行结果再回到调度中心。这一点我后来体会很深:调度器不是一个帮忙分发任务的工具类,它其实是整个系统的中枢。因为哪些人还没完成、哪些电梯还能接、维护结束后谁重新变成可分配状态,这些都得它统一判断。
第三次作业难度再上一个台阶,因为调度器不再只是回答“哪部电梯来接”,还要回答“这次送到哪里为止”。双轿厢改造后,主轿厢和副轿厢服务范围分开,跨区乘客必须在 F2 换乘,所以调度中心实际上做的是分段分配,而不是一次把整趟路直接定死。
我的调度策略不算很激进,主要是几个比较稳的原则。第一,优先选代价更小的电梯,代价里既看它离乘客起点有多远,也看它当前手里已经压了多少任务。第二,电梯已经有方向的时候,尽量顺着当前方向继续跑,而不是频繁折返。第三,能顺路捎带就顺路捎带,减少空跑和重复开关门。第四,维护、改造、回收这类特殊状态一旦进来,就先保证状态合法,普通乘客调度往后让。
如果从性能指标去看,这套策略对时间和电量都有一些帮助。少折返、少空跑、顺路接人,本身就会减少总移动次数和开门次数。但我做到后面越来越确定一件事:二单元后两次作业里,正确性比性能更重要。像维护期间还能不能 RECEIVE,双轿厢能不能同时碰 F2,这些一旦错了,前面做的所有优化都没意义。
##三、 我碰到过的 bug 和怎么 debug
这三次作业里,我印象最深的 bug 基本都不是语法错误,而是时序错误。
最典型的一类就是线程明明应该停下来了,但实际上还在继续走,或者明明应该醒了,却一直在等。这类问题通常和结束条件、等待条件、唤醒时机有关。本地小数据有时候完全看不出来,一到互测或者强测才炸。
第二类 bug 是特殊状态和普通调度打架。比如维护或者改造已经来了,但电梯还在按普通逻辑继续接客,这种错误看起来像是“少判断了一个 if”,实际上背后是状态优先级没理清楚。后来我才慢慢改成:每一轮循环先看特殊状态,再看普通接客逻辑。
第三类 bug 是中途放客之后没有把后续状态收干净。维护、回收、换乘都会出现这种情况:乘客形式上已经 OUT-F 了,但如果旧分配没清、当前位置没更新、或者没有重新送回调度中心,后面就可能重复分配,也可能直接丢人。
第四类 bug 是双轿厢共享冲突。这个是第三次作业里最烦的一类。我保存下来的错误日志里,最典型的一条就是同井道两部轿厢同时到达 F2。这个 bug 对我帮助反而挺大,因为它逼着我承认:有些约束根本不属于单个对象,而属于“两个线程共同竞争的一块资源”。
我后来调多线程 bug 的办法基本固定下来了。先看第一条非法输出是什么,不急着改代码;然后顺着时间往前翻,找出它之前哪一次状态变化就已经不对了;再把问题归结到某条不变量上,比如“进入 F2 前必须先检查对方状态”“中途下客后必须重新入队”“结束前所有线程都要重新判断条件”。如果日志太长,我就缩小输入,把大场景压成一个能稳定复现的小样例。相比打断点,我更依赖日志和不变量,因为断点经常会把原来的线程切换顺序打乱。
二单元做完之后,我对线程安全的理解比之前实在多了。以前我容易把线程安全理解成“哪里危险就加锁”,现在我更在意的是:这份共享状态到底属于谁,谁有权改,别人通过什么方式看到它。
如果把这个问题想清楚了,很多设计会自然一些。全局待分配乘客和系统结束条件,由调度中心管;单部电梯的方向、载重、轿厢内乘客,由电梯线程自己管;同井道双轿厢的冲突,由井道控制器管。谁拥有状态,谁就负责维护它的不变量。
层次化设计对第三次作业特别重要。因为第三次作业一旦引入维护、改造、回收和换乘,如果前面所有逻辑都揉在一起,后面基本只能硬补。反过来,如果输入、调度、执行、共享资源协调这几层分开了,后面新增内容虽然也难,但至少还有地方可以接进去。
我自己这三次作业最大的结构收获,其实不是某个策略,而是知道了“复杂并发程序必须分层”。不分层,后面所有新增需求都会变成互相污染。
这一单元里,我主要把大模型当成辅助思考工具,用得比较多的是 ChatGPT 模型。它们对我最有帮助的地方,不是直接替我把代码写完,而是帮我节省理解和排查的时间。
第一,它很适合整理规则。二单元题面很长,维护流程、双轿厢范围、换乘规则、状态限制都很多,我会先自己读,再让模型帮我压成更短的检查清单。第二,它在读日志方面确实有帮助,尤其是我已经知道某条输出非法,但还没反应过来前面哪一步埋雷的时候。第三,它能提醒我一些边界情况,比如某个状态切换之后还有没有旧任务残留,或者某个等待条件会不会永远醒不过来。
但我也越来越清楚它的局限。多线程最难的地方在时序,而时序问题恰好是大模型最容易说得很像对的、但其实没完全对的地方。它给出的方案有时逻辑上挺顺,但落到真实工程里,可能忽略了锁的范围、忽略了状态切换次序,或者默认某些事情会按理想顺序发生。所以我现在更愿意把它当成代码审阅和思路补充,而不是正确性的最终来源。
大模型更适合帮我检查、补边角、读日志、提问题。这样用下来效果最好。
第二单元对我来说确实是一个明显的难度拐点。第一单元更多还是对象设计和规则实现,到了第二单元,写代码之外还要一直思考线程之间到底怎么协作,什么时候该等,什么时候该醒,什么时候虽然代码能跑但状态其实已经不合法了。
这个单元最有收获的地方,是它逼着我把“会写类”和“会设计并发系统”区分开。前者更多是写法问题,后者更像是在维护一套一直在变化的系统不变量。尤其到了第三次作业,如果前面结构没搭好,后面改起来会特别痛苦。
当然,痛苦也是真的。最难受的不是写不出,而是有时候你觉得逻辑已经对了,结果换一组数据或者换一次线程切换顺序,它又炸了。那种 bug 很耗耐心,也很考验心态。
总的来说,第二单元虽然折磨人,但训练价值确实很高。它让我第一次比较认真地理解了线程安全、状态机、等待唤醒和系统分层这些东西。