记录一个.NET中的AOT死锁问题
1. 问题现象
项目是 WinUI 3 + Native AOT 发布,在调试时一切正常,但是发布并启动后几秒内一切正常,过几秒后UI 线程随机无响应,处于“模糊状态”——不是崩溃退出,是卡死没反应。

问题的诡异之处在于:
- Debug 模式下运行正常
- 非 AOT 发布正常
- 去掉 里面的json解析调用后不挂起
- 减少for循环次数,只取前 100 条数据,也不挂起

2. 问题假设
假设一:JsonElement 在 AOT 下元数据被裁剪
最初怀疑 System.Text.Json 的 JsonElement 动态访问在 Native AOT 下缺少元数据。但很快被推翻:
同一个 item 上,GetStringProperty 被连续调用了 8 次,每次都涉及 TryGetProperty + GetString + 字符串构造。如果 JsonElement 有元数据问题,第一个 GetStringProperty 就应该崩。
假设二:double 值类型返回引用
TryGetDouble(out var value) 中的 value 是 double,纯值类型,直接拷贝,不引用 JsonDocument 的内部缓冲区。排除。
假设三:对象初始化器语法
把 new QuoteData { ... } 拆成先 new 再逐行赋值,仍然会出现挂起现象,因此排除赋值时序。
假设四:ObservableObject 的 PropertyChanged
QuoteData 继承 ObservableObject,但改成普通自动属性后问题依然存在。排除事件机制。
3. 关键信息
“随机若干秒后才出现”这个观察是关键。如果崩溃是数据层面的,应该在访问那个对象的瞬间触发。几秒的延迟意味着:
- GC 在几秒后运行
- 回收了某块内存或遍历了某个列表
- 撞上了运行时内部的不一致状态
根据进程卡死的堆栈结构,在 dotnet/runtime 的官方 Issue 中找到了类似的问题记录。

dotnet/runtime #104582:
“正在被遍历的集合进入了坏状态——已释放的条目仍然留在列表中。这是移除操作缺少锁的结果,当终结器运行时,它会与向列表中添加条目的操作产生竞争。”
microsoft-ui-xaml #9928:
“应用能启动,看起来挺流畅,直到 UI 线程完全卡死。死锁发生得非常随机,有时能用几分钟,有时只有几秒,但总是在 GC 等待收集完成时发生。”
问题的完整链条:
-
你的代码创建
ProductInfo/QuoteData对象 -
WinUI 3 环境下,这些对象被 CsWinRT 投影为原生对象包装器
-
包装器注册到运行时的全局追踪器列表
-
UI 线程向列表添加条目,GC 终结器线程从列表移除条目
-
移除操作缺少锁保护,两个线程同时操作导致列表内部指针损坏
-
几秒后 GC 再次遍历列表,读到坏指针 → 挂起或访问冲突

为什么“去掉某个调用”就“修好”了
实际上,这不是修复,是降低触发概率。去掉 GetDoubleProperty 或 GetDateTimeProperty 调用,减少了单次遍历期间创建的包装器数量。添加操作的频率降低,与终结器“移除操作”撞上的概率窗口缩小。
同理,只取前 100 条数据、分批处理、本质都是同一个逻辑:降低单次遍历期间的包装器创建密度,避开锁竞争窗口。
4. 解决方法
如果遇到类似问题,在等待运行时更新期间,可以采用分批处理降低触发概率:将大数组拆成小批次,每批处理完后让 UI 线程让出一次,给 GC 完成遍历的机会。
当然这种只是降低竞争概率。单次耗时在毫秒级,具体取决于消息队列繁忙程度,最好的方法当然还是升级到.NET版本;
这个问题已经修复并合入最新 .NET,遇到类似问题的朋友升级项目.NET版本即可 :
- 修复提交记录:
dotnet/runtime@d7948dc— “Fixes hang in WinUI apps published to AOT (#104583)” - 目前合入到 .NET 9 分支,.NET 8 用户升级到最新补丁版本后问题应消失
5. 小结
- 综上来看,这个问题不在你的代码,在 .NET 运行时 AOT 路径下的 ComWrappers 锁竞争
- 同时也与 JSON 解析无关,与“大量创建对象”这个行为有关
- 如果分批处理能规避,也只是降低概率
- 根治需要升级到包含修复的 .NET 运行时版本
参考链接