309
社区成员
发帖
与我相关
我的任务
分享U2的内容聚焦于多线程开发,由于多线程的特性,使得调试以及debug都极难完成的。除开在架构上努力,为保证正确性,搭建合适,拥有足够复杂度的评测机是必须的。本人从hw6开始搭建评测机,并于hw7最终迭代出集合数据生成与测试一体,拥有多功能,强正确性判定的评测机。工作最终存放于GitHub :
本次作业的核心难点在于如何进行架构设计,如何进行数据设计,以满足数据安全,线程安全的要求。最终总结出的核心规则为:公共数据单独成类,由接受类持有,私有数据仍然由线程类本身持有。

最终的逻辑非常简单,是一个单线的数据传递处理过程。InputThread -> Waitqueue -> DispatcherThread -> Requestqueue -> ElevatorThread -> waitqueue 两个线程中夹一个数据类,像三明治一样进行设计,以实现方便的锁管理与数据传递,而且因为此时公共数据有类似单向流通的趋势,直接使用 synchronized 也具有很好的效能。另外,Dispatcher 中对分配效能的逻辑单独分离为 SImulator ,Elevator 中电梯指令判断逻辑单独分离为 Strategy 并最终通过枚举类 Advice 的规则反馈,Elevator 内只有其运行逻辑,这样也能显著简化线程安全管理。
另外,为了方便 Dispatcher 获取 Elevator 的信息以进行更加高效的决策,考虑使用了快照的机制,使用 ElevatorStatue 和 ElevatorTable 来实现。此种设计方法避免了该过程对电梯的的干扰,虽然有数据不同步的问题,但是在分配决策中数据的暂时性不同步是可接受的。
| 类/线程名称 | Cyclic | Dcy | Dcy* | Dpt | Dpt* | PDcy | PDpt |
|---|---|---|---|---|---|---|---|
| ElevatorThread | 0.0 | 9.0 | 11.0 | 1.0 | 2.0 | 1.0 | 1.0 |
| DispatcherThread | 0.0 | 7.0 | 9.0 | 2.0 | 3.0 | 1.0 | 1.0 |
| MainClass | 0.0 | 6.0 | 13.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| InputThread | 0.0 | 5.0 | 5.0 | 1.0 | 2.0 | 1.0 | 1.0 |
| Simulator | 0.0 | 5.0 | 6.0 | 1.0 | 4.0 | 1.0 | 1.0 |
| Strategy | 0.0 | 3.0 | 3.0 | 3.0 | 5.0 | 1.0 | 1.0 |
| MockRequestQueue | 0.0 | 2.0 | 2.0 | 1.0 | 5.0 | 1.0 | 1.0 |
| TestMain | 0.0 | 2.0 | 16.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| ElevatorStatus | 0.0 | 1.0 | 1.0 | 5.0 | 7.0 | 1.0 | 1.0 |
| ElevatorTable | 0.0 | 1.0 | 2.0 | 3.0 | 5.0 | 1.0 | 1.0 |
| RequestQueue | 0.0 | 1.0 | 1.0 | 6.0 | 8.0 | 1.0 | 1.0 |
| TestMain.TimableInputStream | 0.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | 1.0 |
| Waitqueue | 0.0 | 1.0 | 1.0 | 4.0 | 5.0 | 1.0 | 1.0 |
| Advice | 0.0 | 0.0 | 0.0 | 3.0 | 6.0 | 0.0 | 1.0 |
| Person | 0.0 | 0.0 | 0.0 | 9.0 | 12.0 | 0.0 | 1.0 |
| Shaft | 0.0 | 0.0 | 0.0 | 2.0 | 3.0 | 0.0 | 1.0 |
Average | 0.0 | 2.75 | 4.4375 | 2.6875 | 4.3125 | 0.75 | 0.9375 |
| 以上分析中我们可以看到,本次设计复杂度和权重分散均匀,集中于线程类和决策类,说明本次设计完整,逻辑分离且符合OO逻辑 |
初次设计,数据传输仍然是以 PersonRequest 的形式进行传递,未设计 Person 类进行重写。初步实现三明治型数据传递,且实现 Strategy 从 Elevator 中分离。
为保证性能,Strategy 中实现了根据请求自行掉头,以减少重复运动过程。
本次设计删去了输入对乘客所属电梯的分配,增加了检修逻辑。主要更改在于在 Input 和 RequestQueue 中增加 WaitQueue 数据层和 Dispatcher 线程以实现电梯的分配,并增加快照机制以方便 Dispatcher 获取电梯信息以实现对电梯未来运行的模拟,从而实现贪心的分配方式。检修逻辑主要修改存在于电梯类,需要增加检修逻辑。另外特别需要注意的是,由于检修的存在,对乘客的重新分配成为了必须实现的内容,于是修改 Dispatcher 线程的结束逻辑,增加所有检修已结束的约束。
本次设计增加了双桥箱的设计。双桥箱设计的核心点在于增加电梯数量,修改电梯启动逻辑与实现对公共楼层(2楼)的管理。对公共楼层的管理可以通过新增类 Shaft 来进行实现,Shaft 的核心功能是模拟一个锁以实现对公共楼层的保护。另外还需要修改电梯的运行逻辑和指令逻辑,以实现对跨界请求的处理(跨界请求到公共层下车)
由于双桥箱中,单个电梯无法绝对完成每个完整请求,原先的 Dispatcher 逻辑需要修改。为了尽可能复用原先 Simulator 的模式,可以考虑对双桥箱电梯进行特殊判断。如果无法完成全程,就以完成可完成部分再加上修正时间来计时。并可以经对比测试,调整修正时间。
一遍过,爽歪歪
好了孩子们,开始恶心人了。
由于快照是无法实时更新的,导致如果在同一时间戳同时出现MAINT和分配请求,可能会出现刚刚开始检修的电梯立刻被再次分配请求,从而干扰其重新分配。
可能出现5个电梯在检修,所有的请求只能分配给一个电梯。此时如果涌入需求过多,在后续过程中会出现只有这一个电梯在运行的情况,从而导致TLE。解决方法为限制每个电梯 RequestQueue 的最大size ,从而规避该类特殊情况
对recieve的机制理解错误 recieve 不能直接废弃,所以MAINT中的重新分配过程必须要在其输出BEGIN,自动废弃剩余 recieve 后才能进行。
感谢评测机,感谢评测机,评测机真是太棒啦!!
在对2楼的管理,要特别注意在输出 Arrive 前申请锁,在离开输出 Arrive后再还锁,从而避免出现可能的在同一层的情况。
在recycle的时候,重新设置电梯最高楼层最低楼层的时候写反了……
重写 Dispatch 的时候给hw6实现的电梯最大size机制漏掉了……
hw7的实现确实很下饭,大家一定要注意重写的时候不要忘记原有的功能啊……
鉴于已经构建了较为强大的评测机,那必然要好好利用。通过对同房间的同学进行大量测试,成功发现巨量错误数据,取得巨大收益。
以下展示在某100次评测中发现的巨量bug,而且其中相当部分在提交后都成功hack:

另外,在自己实现的时候发现的问题也很值得作为特殊情况进行打击。例如hw6中发现的所有请求涌入同一个电梯的情况,构造数据后成功叉满全场,爽歪歪!
1.电梯时间戳优化
long startTime = System.currentTimeMillis();
/*
*/
long elapsedTime = System.currentTimeMillis() - startTime;
long sleepTime = Math.max(0, 400 - elapsedTime);
if (sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
事实上,我们需要实现的是两次输出的间隔为400ms,并不是一定要sleep400ms。运用代码中获取时间戳的方法,能够很舒服的节省中间的运行时间。
2.影子电梯:
private int selectBestElevator(Person request) {
int bestId = 0;
double minTime = Double.MAX_VALUE;
for (int id : ElevatorTable.getInstance().getAllIds()) {
ElevatorStatus status = ElevatorTable.getInstance().getStatus(id);
if (status == null || status.isMaintaining()
|| status.getWaitingPeople().size() >= maxMember) {
continue;
}
double time = Simulator.estimate(status, request);
if (time < minTime) {
minTime = time;
bestId = id;
}
}
return bestId;
}
通过对电梯未来运行的模拟,选取加入新请求后总运行时间最短的一个加入,从而在选取局部最优解,并由于贪心很大概率这也是最终较优解。3.尽可能再次分配
在MAINT和双桥箱逻辑中,可以重新分配尽可能重新分配,以实现局部最优向全局最优靠拢。
很可惜,在本次单元中,我的AI使用比例有较大提升。一方面是多线程的机制难度迫使我花费更多精力在学习和架构上,打磨代码的时间确实减少;另一方面是对于多线程难以调试的困境,在初次写的时候还是太需要AI为我检查有无线程风险等问题了。
本人使用的AI为Gemini fast (Google打死不给我账号)。可以用于对话优化架构,询问设计是否满足线程安全等,但是代码直接生成还是比较拉跨。
以下是我的代码AI使用度量分析:
| 作业 | 正确性相关代码AI生成比例 | 性能优化代码AI生成比例 |
|---|---|---|
| 第五次 | 10% | 10% |
| 第六次 | 25% | 10% |
| 第七次 | 25% | 20% |
U2不同于U1,难以明显的得到性能最优解。事实上,无论你做如何优化,在最终结果出来之前你都很难知晓你的优化性能如何。
对于此情况,特别在hw7,对分配程序进行适当的调参是非常必要的,我根据最终重复评测统计分析后,选择对双桥箱只跑部分的楼层进行了2s的时间修正,得到了相对很好的效果。
以及AI,我的天哪Gemini大人,吓哭了,我写一个hw要10h左右,搭个评测机对拍它理解整个过程并写出完整正确性验证代码对话一小时就搞定了。更不要说互测的时候查到的神人代码,ai生成的正确性已经上升到1k行等级的代码了,不由得让人思考我们OO训练面对的严酷前景。
恶心全场的邪恶多线程单元终于结束了……活下来了各位
细致入微,热心分享,点赞!