2026-09-10 综合博客:异步异常 + 排查核实思维
今天的内容分两大块:语法 (异步方法的异常去哪了)和思维(排查「查不到/超时」、核实「功能到底实现没」)。代码全部示意化(脱敏),语法用真名。
上半场 · 语法:异步方法的异常去哪了
同样一个会抛异常的异步方法,调用姿势不同,异常的去向完全不同:
调用一个 async 方法,异常抛出来走哪?
① 不 await(发完不管,返回的 Task 被丢弃)
└─→ 后台 UnobservedTaskException(GC 回收时才触发,没人接 = 静默)
② async void / UI 线程上同步抛
└─→ UI 线程 DispatcherUnhandledException(e.Handled 这个开关在这才有效)
类比:报警器响了,声音传给谁,取决于「接线接到哪」。接后台值班室(没人),就是 ①------响了个寂寞;接前台保安亭(有人守),就是 ②。
今天那个「该弹窗却没弹」的坑,本质就是异常走了 ①,而兜底接在 ②------线没接对,当然没反应。
坑 1:不 await(fire-and-forget)
概念 :调用异步方法但不 await、也不接返回值,方法里抛异常就回不到调用方。
类比:托人送快递不留回执------快递半路丢了,没人通知你。
csharp
// ❌ 不 await:异常装进被丢弃的 Task,回不来
void 点按钮() { 触发动作(); } // 里面抛异常,这里接不住
// ✅ await + try-catch:把异常拉回来
async Task 点按钮()
{
try { await 触发动作(); }
catch (Exception e) { 弹窗(e.Message); }
}
误区:别以为「没 await 就是走 UI 线程报错」------方向反了,没 await 恰恰是抛到后台去。
坑 2:ConfigureAwait(false) 切到线程池
概念 :默认 await 结束后会切回原上下文(如 UI 线程);加 .ConfigureAwait(false) 就是「别切回来」,后续代码可能落线程池。
类比 :UI 线程和线程池是两拨人------UI 线程只有一条,专门管界面(画控件、接点击);线程池是一堆后台线程,Task/await 默认在里面排队干活,不管界面。
csharp
await 干活(); // 默认:切回 UI 线程继续
await 干活().ConfigureAwait(false); // 不切回,可能落线程池
坑 :ConfigureAwait(false) 之后直接弹窗,代码可能已落在后台线程池,一弹窗就「跨线程访问界面」报错。修法:弹窗外面套 Dispatcher.Invoke 强制切回 UI 线程。
坑 3:空 catch 静默失败 + catch 三件套
概念 :catch (Exception) { } 空 catch------把异常吞掉,不记日志不提示,程序照跑,用户无感知。这是「静默失败」最典型的形态。
类比:火警响了,但报警线被剪断------火在烧,没人听见。
正确姿势:catch 三件套(缺一不可)
csharp
catch (Exception e)
{
记录日志(e); // ① 留痕,事后能查
Application.Current.Dispatcher.Invoke(() =>
弹窗(e.Message)); // ② 切 UI 线程 ③ 让用户看见
}
只弹窗不记日志 → 事后查不到;只记日志不弹窗 → 现场没人发现。
下半场 · 排查与核实思维
一、「查不到数据」其实是「被过滤了」
------别急着归因「断了」。
二、超时是「0 帧」不是「慢」
现象:任务报超时。
真根因:不是设备慢,是「0 帧」。上一个位置正常收满,下一个位置触发时 0 帧------因为每次扫完都把数据流停掉再重启,下次触发时设备还没就绪,错过了触发。
类比:水龙头每次用完都关掉,下次开要等水重新流出来;动作太快就接不到一滴水。不是水流得慢,是「根本没接到」。
关键区分:「0」和「慢」是两码事。0 帧 = 没触发(流没就绪);慢 = 真卡(帧慢慢到)。看日志先分清是「0」还是「慢」,别把「没触发」当「性能差」。
三、核实「功能实现了没」要追到调用处
今天核实功能时踩了两次「下结论太快」的坑,教训都指向同一件事:
- 看到「过滤条件写死排除某类」→ 别急着说「没实现」,要追它读的是不是配置(可能配置默认关,所以现场看起来像没有)。
- 看到「只写库」→ 别急着说「没有独立文件」,要追提交之后有没有自动导出。
类比:看到墙上开关是关的,别以为「没装开关」------要查它后面到底接没接线、线通到哪。开关默认关 ≠ 没这功能。
一句话 :核实「实现了没」,必须追到调用处 / 配置来源,不能停在过滤条件或方法内部。
四、消息驱动解耦(概念)
概念:按钮做完事,只「广播」一条消息,别的模块自己订阅、自己响应。按钮代码完全不知道响应方存在。
类比:广播------播音员不知道谁在听,谁听谁行动。好处是「非侵入」:加新功能不用改按钮代码,新模块自己挂到广播上就行。
语法附录
async voidvsasync Task:Task 能 await/catch;void 不能,异常强抛回 UI 线程。awaitvs 不await:不 await = fire-and-forget,异常进UnobservedTaskException。ConfigureAwait(false):后续代码不切回原上下文,可能落线程池。DispatcherUnhandledException:UI 线程异常处理器,e.Handled=true标记已处理、静默。UnobservedTaskException:被丢弃 Task 的异常,GC 回收才触发,时机不靠谱。Dispatcher.Invoke:从后台线程池切回 UI 线程(弹窗前包一层)。- 空
catch {}:静默失败,修法 = catch 三件套(记日志 + 切 UI 线程 + 弹窗)。
结尾
今天一整天,绕来绕去就两个词:「没反应」和「查不到」。
- 语法侧:异步异常走错路、没人接 → 静默。记住两条路 + catch 三件套。
- 排查侧:「查不到」先分清是「断了」还是「被过滤」;「超时」先分清是「0」还是「慢」;「核实」要追到调用处,别停在表面。
一句话:碰到「没反应却没报错」,先问「异常走哪条路、有没有人接」;碰到「查不到」,先问「查错表没、被过滤没」------别急着归因,先分清表象下的两码事。