跳到主要内容

记录一个.NET中的AOT死锁问题

· 阅读需 4 分钟
作者:小烨App

1. 问题现象​

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

ScreenShot_2026-09-23_205333_236

问题的诡异之处在于:

  • Debug 模式下运行正常
  • 非 AOT 发布正常
  • 去掉 里面的json解析调用后不挂起
  • 减少for循环次数,只取前 100 条数据,也不挂起

ScreenShot_2026-09-23_202850_261

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 等待收集完成时发生。”

问题的完整链条:

  1. 你的代码创建 ProductInfo / QuoteData 对象

  2. WinUI 3 环境下,这些对象被 CsWinRT 投影为原生对象包装器

  3. 包装器注册到运行时的全局追踪器列表

  4. UI 线程向列表添加条目,GC 终结器线程从列表移除条目

  5. 移除操作缺少锁保护,两个线程同时操作导致列表内部指针损坏

  6. 几秒后 GC 再次遍历列表,读到坏指针 → 挂起或访问冲突

    deadlock

为什么“去掉某个调用”就“修好”了​

实际上,这不是修复,是降低触发概率。去掉 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 运行时版本

参考链接