WPF 死锁:复现与验证
配套文章:WPF 中的死锁。
需要 Windows 和 .NET 10 SDK,无第三方包依赖。在本目录运行:
dotnet run
程序创建真实的 WPF Dispatcher,但不显示窗口。每个场景在独立子进程中运行;独立监控线程检查操作完成状态和 Dispatcher 心跳,然后结束该子进程。父进程只回收自己启动的子进程,并汇总结果。成功返回退出码 0,失败返回非零退出码。
UiBlocked 表示受控等待建立后 UI 心跳停止;OperationBlocked 表示业务操作无法完成,但 UI 心跳仍在运行。后者也包含无人运行消息循环的队列挂起,用于和真正的循环等待区分。用来同步参与者的信号建立等待关系,监控中的短暂延时只用于采样,不用来决定锁获取顺序。
| 场景 | 验证内容 |
|---|---|
invoke-wait |
UI 同步等待后台,后台同步调用 UI |
result-context |
.Result 阻塞恢复异步方法所需的 UI 上下文 |
dispatcher-task-wait |
UI 同步等待自己队列里尚未执行的任务 |
lock-invoke |
后台持锁等待 UI,UI 委托需要同一把锁 |
semaphore-recursion |
异步嵌套获取同一个许可,UI 仍有心跳 |
async-task-cycle |
两个任务互相异步等待 |
two-dispatchers |
两个 STA Dispatcher 同步回调彼此 |
unserviced-dispatcher |
向没有消息循环的工作线程 Dispatcher 排队 |
await-fix |
UI 异步等待期间继续处理心跳,之后恢复 UI 上下文 |
snapshot-fix |
取得快照、释放锁后再调度 UI |
semaphore-fix |
只获取一次许可,取消获取时不额外释放 |
begininvoke-order |
UI 同步等待结束后的代码先于已排队的更新执行 |
configureawait-completed |
已完成任务上的 ConfigureAwait(false) 不强制换线程 |
configureawait-caller |
库内部不捕获 UI,不影响 UI 调用者自己的捕获 |
dispatcher-inner-task |
外层 Dispatcher 操作先完成,Unwrap 等待内层任务 |
timeout-keeps-operation |
超时不自动取消或完成原任务 |
也可以运行一个场景:
dotnet run -- --case semaphore-recursion
它同样由监控线程结束,不会把终端无限留在死锁中。不要去掉隔离和监控后直接把反例集成进日常应用。
检查文章中的全部 C# 片段是否可编译,并核对目录锚点和代码围栏:
./VerifyArticle.ps1
该脚本直接读取文章的 14 段 C# 代码,只为成员片段补上窗口类、命名空间和文中沿用的依赖。编译项目写入系统临时目录,位置会在输出中显示,不修改文章或把生成代码加入仓库。代码语法同时按 C# 7.3 检查;这不代表所有使用的框架 API 都适用于任意旧版 .NET Framework。
这些检查验证具体的等待机制和示例代码,不代替真实应用的集合绑定、窗口关闭、第三方控件与 COM/Win32 集成测试。UI 心跳也只验证 Dispatcher 是否推进,不能证明整个界面的性能和正确性。