news 2026/10/2 7:00:57

退出防呆:用“总闸“收口所有退出入口,以及三个并发语法坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
退出防呆:用“总闸“收口所有退出入口,以及三个并发语法坑

退出防呆:用"总闸"收口所有退出入口,以及三个并发语法坑

一、引子

工业软件最怕一种操作:机器正在跑活,有人顺手点了"退出",程序二话不说直接关了——PLC 还在动、轴还在走、下料还没完,现场一下失控。

所以得有"退出防呆":退出前先问一句"活干完没",没干完就拦住、先把尾收干净再关。

但"防呆"自己也有坑。本文讲两件事:怎么把所有退出入口收口到一处(架构),以及收口时踩到的三个并发语法坑(重点)。

二、架构:一栋楼十几个门,全接同一个总闸

程序里能退出的入口不止一个:主窗口右上角的 X、键盘 Alt+F4、还有就绪/编程/标定/设置四个页面各自带的"退出"按钮。

主窗口 X / Alt+F4 ─→ 窗口 Closing 事件 ─┐ ├─→ 退出守卫服务(全局单例) 四个页面的「退出」按钮 ──────────────────┘

生活类比:一栋楼有十几个门,每个门都能走出去。与其给每个门单独装锁,不如把所有门都接到同一个总闸——拉闸才真断电,比逐个门管稳得多。这个"总闸"就是一个全局单例的服务,所有退出请求先过它。

2.1 为什么必须收口(核心机制)

关键在于:框架的"整程序关闭"会无视窗口 Closing 事件里的取消标记。

退出入口底层调用取消标记生效?
点窗口 X / Alt+F4关单个窗口✔ 能拦住
程序里的退出按钮直接关整个应用✘ 被忽略

也就是说,只靠主窗口的 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(); };

三个要点:

  1. e.Cancel要在第一个await之前设好——否则框架已经认为本次关闭处理完了,设了也没用;
  2. await Task.Yield()是必需的——先让框架把本次关闭流程处理干净,否则紧接着若走到关闭,会撞上"窗口正在关闭";
  3. async void里抛异常会落到全局未处理异常通道——所以这条链上要自己catch,别让它漏到顶层。

(这点和await的基础有关,但陷阱在"事件处理器里的顺序",与"挂起/恢复"是两码事。)

六、方法论:并发偶发失败,怎么下结论

单测里偶发多了一条失败,要判是不是自己改出来的,别急着认:

  1. 先隔离——单跑那条,通过就不像代码缺陷;
  2. 加大样本——同一份代码连跑多次;
  3. 做对照——把自己的改动暂存掉,连跑同样多次;
  4. 关变量——关掉测试的并行执行,排除"测试间互相干扰"。

经验:只跑两三次对照容易得出错误归因;能用"关变量"排除干扰,比堆样本更有力。本场数据就显示:关掉并行后偶发消失,说明是测试间并行干扰,而非代码缺陷。

七、小结

  1. 退出要收口到总闸——框架的"整程序关闭"会无视窗口取消标记,只拦主窗口拦不住页面按钮,必须把所有退出入口汇到同一个守卫服务。
  2. WhenAny是竞速不是体检——必须显式判IsFaulted,否则收尾失败和成功在日志里一样,成了静默失败。
  3. ??=缓存 Task 要防"焊死"——同步快路径会把已完成任务写回字段,闸门判"是否已完成"才稳。
  4. async void事件里await讲顺序——e.Cancel先设、Task.Yield垫步、自己catch。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 7:00:19

用 Ace Data Cloud 快速接入 Kling 服装视频复刻:电商短视频生产的新选择

用 Ace Data Cloud 快速接入 Kling 服装视频复刻:电商短视频生产的新选择 如果你正在做电商、内容营销、品牌种草或素材自动化生产,最耗时的环节往往不是“想一个创意”,而是把商品图、模特图、参考视频、配音和成片流程真正串起来。传统方式…

作者头像 李华
网站建设 2026/10/2 7:00:09

数据库的盘搬不上对象存储?三种存储的分界线,一篇说清

评审存储方案的时候,有三类需求最常见:把 MySQL 的数据目录挪过去、给虚拟机提供磁盘、给全公司当一个共享网盘。这三类需求听着都像"存东西",但对象存储对其中两类是接不住的。先分清三种存储各自承诺了什么,再决定要不…

作者头像 李华
网站建设 2026/10/2 6:57:29

SpringBoot+Vue 毕设项目开发踩坑记录(前后端分离)

最近在做一套 SpringBootVue 的本科毕业设计管理系统,过程踩了不少坑,在这里记录下来,给做同类毕设的同学参考。 1. 跨域问题 前后端分离项目最常见的就是跨域报错。很多新手直接在前端配置代理,部署到服务器之后就失效。 推荐方案…

作者头像 李华