
退出防呆用总闸收口所有退出入口以及三个并发语法坑一、引子工业软件最怕一种操作机器正在跑活有人顺手点了退出程序二话不说直接关了——PLC 还在动、轴还在走、下料还没完现场一下失控。所以得有退出防呆退出前先问一句活干完没没干完就拦住、先把尾收干净再关。但防呆自己也有坑。本文讲两件事怎么把所有退出入口收口到一处架构以及收口时踩到的三个并发语法坑重点。二、架构一栋楼十几个门全接同一个总闸程序里能退出的入口不止一个主窗口右上角的 X、键盘 AltF4、还有就绪/编程/标定/设置四个页面各自带的退出按钮。主窗口 X / AltF4 ─→ 窗口 Closing 事件 ─┐ ├─→ 退出守卫服务全局单例 四个页面的「退出」按钮 ──────────────────┘生活类比一栋楼有十几个门每个门都能走出去。与其给每个门单独装锁不如把所有门都接到同一个总闸——拉闸才真断电比逐个门管稳得多。这个总闸就是一个全局单例的服务所有退出请求先过它。2.1 为什么必须收口核心机制关键在于框架的整程序关闭会无视窗口 Closing 事件里的取消标记。退出入口底层调用取消标记生效点窗口 X / AltF4关单个窗口✔ 能拦住程序里的退出按钮直接关整个应用✘ 被忽略也就是说只靠主窗口的 Closing 事件只能拦住点 X拦不住程序里的退出按钮。这就是为什么四个页面的按钮也必须改走同一个退出守卫——否则防呆在这几个入口上是空的。2.2 两条流程无在途作业 → 不弹框、不转圈 → 关光 → 关闭 有在途作业 → 二次确认框 →「是」→ 全屏转圈 → 收尾停 PLC 异常恢复下料有超时上限 → 收尾完 → 关闭生活类比有人在屋里干活时你想锁门走人得先问一句活干完没没人就直接走有人就等他收完。2.3 判据的边界提醒有没有在途作业的判据只认生产运行/单次运行不认实时预览忙不忙、也不认标定进行中。这是个有意无意的盲区——判据越窄漏拦的风险越大设计时要心里有数。三、语法坑一Task.WhenAny只报谁先到不报谁摔了收尾时要在收尾任务和超时之间二选一很自然写成Task 收尾 取消收尾流程(); if (await Task.WhenAny(收尾, Task.Delay(超时阈值)) ! 收尾) { 记超时; }坑WhenAny只告诉你谁先完成不告诉你是不是成功。如果收尾自己抛异常挂了它会被当成已完成——于是不打任何日志就走下去。结果收尾失败和收尾成功在日志里长得一模一样。生活类比运动会裁判只拿喇叭喊第一名到了“不喊那个人中途摔了”。你以为人都顺利到了其实有人栽在半路系统却当没事发生。正确写法——显式看故障状态Task 完成 await Task.WhenAny(收尾, Task.Delay(超时阈值)); if (完成 ! 收尾) { 记超时; } else if (收尾.IsFaulted) { 记收尾异常; } // ← 不能漏一句话WhenAny是竞速不是体检。要判成败自己看IsFaulted。四、语法坑二??缓存 Task会把闸门永久焊死想让同一操作并发进来时共享同一个任务容易顺手写// ❌ 有坑 public Task 执行() _任务 ?? 核心异步();坑??是先算右边、再赋值。如果核心异步()一路同步跑完中间没有真正挂起的等待它会执行到末尾可能顺手把自己清理掉然后??才把一个已完成的任务写回字段——之后每次调用都拿到这个已完成的任务闸门被永久占住方法再也不真正执行。生活类比你想在第一次有人按门铃时才装个闸门结果门铃一响装闸门的人发现门早就开着、装完直接把门已开的牌子焊死在杆上。以后所有人看牌子都以为门开着再也没人真去关门。什么时候会一路同步跑完等待一个已经完成的任务不会切换线程走同步快路径。比如无在途作业时整条收尾几乎全是同步路径——恰恰是最高频的常态。修法——闸门同时判是否已完成// ✔ public Task 执行() { if (_任务 is null || _任务.IsCompleted) // 已完成的旧任务不算进行中 _任务 核心异步(); return _任务; }五、语法坑三轻async void事件里await的顺序窗口 Closing 是个async void的事件处理器。要在这里await收尾顺序有讲究Closing async (sender, e) { e.Cancel true; // ← 必须在第一个 await 之前同步设好 if (!确认退出()) return; await Task.Yield(); // ← 让框架先把本次关闭处理完 await 收尾并关闭Async(); };三个要点e.Cancel要在第一个await之前设好——否则框架已经认为本次关闭处理完了设了也没用await Task.Yield()是必需的——先让框架把本次关闭流程处理干净否则紧接着若走到关闭会撞上窗口正在关闭async void里抛异常会落到全局未处理异常通道——所以这条链上要自己catch别让它漏到顶层。这点和await的基础有关但陷阱在事件处理器里的顺序与挂起/恢复是两码事。六、方法论并发偶发失败怎么下结论单测里偶发多了一条失败要判是不是自己改出来的别急着认先隔离——单跑那条通过就不像代码缺陷加大样本——同一份代码连跑多次做对照——把自己的改动暂存掉连跑同样多次关变量——关掉测试的并行执行排除测试间互相干扰。经验只跑两三次对照容易得出错误归因能用关变量排除干扰比堆样本更有力。本场数据就显示关掉并行后偶发消失说明是测试间并行干扰而非代码缺陷。七、小结退出要收口到总闸——框架的整程序关闭会无视窗口取消标记只拦主窗口拦不住页面按钮必须把所有退出入口汇到同一个守卫服务。WhenAny是竞速不是体检——必须显式判IsFaulted否则收尾失败和成功在日志里一样成了静默失败。??缓存 Task 要防焊死——同步快路径会把已完成任务写回字段闸门判是否已完成才稳。async void事件里await讲顺序——e.Cancel先设、Task.Yield垫步、自己catch。