C#由窗体原子表溢出造成的软件闪退,根本原因补充

引言

在2024年9月28号我写了一篇关于C#由窗体原子表溢出造成的软件闪退的博客,那时候由于工作的原因,并且客户响应的问题,我没有深究具体原因的时间。在前段时间,我又重新遇到了相同的问题,恰好最近软件逐渐趋近于稳定,空余时间也多了,所以再次遇到这个问题的时候,我重新去深究这个问题。

补充一下我之前的博客链接:C#由窗体原子表溢出造成的软件闪退的问题解决方法_c#程序闪退怎么办-CSDN博客

结论

先说结论,经过我一段时间的分析和查WPF的源码,我发现是Geometry的底层调用有着直接的关系。这里放一个我完美复现原子表节点溢出的情况:Github地址:https://github.com/2825077535/Geometey_question.git

并且同步附上WPF的源码:https://github.com/dotnet/wpf.git

从这个我写的Demo截图就能直接发现问题点。我创建了500个线程,对应原子表也有500个节点,所以原子表节点异常一定是与线程的创建有关的。

那么我下面直接拿WPF的源码直接分析是哪个地方会导致这个问题。

一、WPF问题原因

  1. `Geometry.Parse(string)` 并不是纯字符串解析,它会在解析过程中创建 `StreamGeometry`,而 `StreamGeometry -> Geometry -> Animatable -> Freezable -> DependencyObject -> DispatcherObject` 整条继承链都属于 WPF 对象体系。

  2. `DispatcherObject` 的构造函数会直接绑定 `Dispatcher.CurrentDispatcher`。因此,只要某个线程第一次在该线程上创建 WPF `DispatcherObject`,就可能触发该线程的 `Dispatcher` 自动创建。

  3. `Dispatcher` 在构造时会创建一个 `MessageOnlyHwndWrapper`,而 `MessageOnlyHwndWrapper` 底层又会走到 `HwndWrapper`,其中会:

  • 生成带 GUID 的唯一窗口类名;

  • 调用 `RegisterClassEx` 注册窗口类,拿到 `_classAtom`;

  • 调用 `CreateWindowEx(..., HWND_MESSAGE, ...)` 创建"消息专用窗口"。

  1. 因此,Atom Table 中持续上涨的节点,真正持续增长的核心来源不是 `Geometry` 本身,而是"后台线程第一次触碰 WPF 对象 -> 自动创建 Dispatcher -> 创建 `MessageOnlyHwndWrapper` -> 注册唯一窗口类"的副作用。

  2. `Geometry.Parse` 返回前确实会把生成的 `StreamGeometry` `Freeze()`,而 `Freeze()` 内部会 `DetachFromDispatcher()`,所以返回对象最终变成 free-threaded;但这只发生在 Dispatcher 已经被创建之后,无法回滚前面已经创建的 Dispatcher / hidden HWND / class atom。

二、源码链路

  1. `Geometry.Parse`

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\Generated\Geometry.cs`

关键位置:299-303

cs 复制代码
public static Geometry Parse(string source)
{
    IFormatProvider formatProvider = TypeConverterHelper.InvariantEnglishUS;
    return MS.Internal.Parsers.ParseGeometry(source, formatProvider);
}
  1. `ParseGeometry` 创建 `StreamGeometry`

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\ParsersCommon.cs`

关键位置:73-86

cs 复制代码
internal static Geometry ParseGeometry(string pathString, IFormatProvider formatProvider)
{
    FillRule fillRule = FillRule.EvenOdd;
    StreamGeometry geometry = new StreamGeometry();
    StreamGeometryContext context = geometry.Open();
    ParseStringToStreamGeometryContext(context, pathString, formatProvider, ref fillRule);
    geometry.FillRule = fillRule;
    geometry.Freeze();
    return geometry;
}

这里的关键点是:一进入解析就先 `new StreamGeometry()`,所以即使后面只是做路径字符串转几何,前面也已经进入 WPF 对象创建链路。

  1. `StreamGeometry` 属于 WPF Freezable/DispatcherObject 体系

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\StreamGeometry.cs`

关键位置:20

cs 复制代码
public sealed partial class StreamGeometry : Geometry

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\Generated\Geometry.cs`

关键位置:30

cs 复制代码
public abstract partial class Geometry : Animatable

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\Animation\Animatable.cs`

关键位置:13

cs 复制代码
public abstract partial class Animatable : Freezable

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Freezable.cs`

关键位置:18

cs 复制代码
public abstract class Freezable : DependencyObject

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs`

关键位置:41

cs 复制代码
public class DependencyObject : DispatcherObject
  1. 真正触发线程绑定的是 `DispatcherObject` 构造函数

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\DispatcherObject.cs`

关键位置:121-124

cs 复制代码
protected DispatcherObject()
{
    _dispatcher = Dispatcher.CurrentDispatcher;
}

这说明:只要在某线程上创建 `StreamGeometry`,最终都会执行到这里,从而访问 `Dispatcher.CurrentDispatcher`。

  1. `CurrentDispatcher` 会在当前线程没有 Dispatcher 时自动创建

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\Dispatcher.cs`

关键位置:44-58

cs 复制代码
public static Dispatcher CurrentDispatcher
{
    get
    {
        Dispatcher currentDispatcher = FromThread(Thread.CurrentThread);
        if(currentDispatcher == null)
        {
            currentDispatcher = new Dispatcher();
        }
        return currentDispatcher;
    }
}

因此,在"非 UI 线程首次解析 Geometry"时,如果该线程以前没碰过 WPF `DispatcherObject`,这里就会直接 `new Dispatcher()`。

  1. `Dispatcher` 构造函数会创建消息专用窗口

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\Dispatcher.cs`

关键位置:1717-1740

cs 复制代码
private Dispatcher()
{
    _queue = new PriorityQueue<DispatcherOperation>();
    _tlsDispatcher = this;
    _dispatcherThread = Thread.CurrentThread;
    lock(_globalLock)
    {
        _dispatchers.Add(new WeakReference(this));
    }
    _defaultDispatcherSynchronizationContext = new DispatcherSynchronizationContext(this);
    _window = new MessageOnlyHwndWrapper();
    _hook = new HwndWrapperHook(WndProcHook);
    _window.AddHook(_hook);
}

关键结论:Dispatcher 不是轻量标记对象,它会直接创建一个隐藏的消息窗口。

  1. `MessageOnlyHwndWrapper` 明确就是 message-only window

文件:`src\Microsoft.DotNet.Wpf\src\Shared\MS\Win32\MessageOnlyHwndWrapper.cs`

关键位置:8-10

cs 复制代码
internal class MessageOnlyHwndWrapper : HwndWrapper
{
    public MessageOnlyHwndWrapper() : base(0, 0, 0, 0, 0, 0, 0, "", NativeMethods.HWND_MESSAGE, null)

`HWND_MESSAGE` 说明它不是可见窗口,而是消息专用窗口。所以即使"没有弹窗",底层仍然会创建 HWND 相关资源。

  1. `HwndWrapper` 为什么会制造 Atom Table 节点

文件:`src\Microsoft.DotNet.Wpf\src\Shared\MS\Win32\HwndWrapper.cs`

关键位置:88-116

cs 复制代码
_classAtom = 0;
string randomName = Guid.NewGuid().ToString();
string className = String.Format(..., "HwndWrapper[{0};{1};{2}]", appName, threadName, randomName);
...
_classAtom = UnsafeNativeMethods.RegisterClassEx(wc_d);
_handle = UnsafeNativeMethods.CreateWindowEx(exStyle, className, name, style, ... parent, ...);

这里非常关键:

  • `className` 带 `Guid.NewGuid()`,每次都是全新的窗口类名;

  • `RegisterClassEx` 会把该窗口类注册进 Win32 atom table,并返回 `_classAtom`;

  • 随后 `CreateWindowEx` 用这个类名创建窗口。

所以每创建一个新的 `HwndWrapper`,通常就会新增一个新的类 atom。因为类名是唯一 GUID,所以不是复用旧类,而是持续注册新类。

三、为什么是 HwndWrapper 的 atom 在涨,而不是别的

  1. `Dispatcher` static ctor 里还有:

文件:`Dispatcher.cs` 22-31

cs 复制代码
_msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");
  1. `HwndWrapper` static ctor 里还有:

文件:`HwndWrapper.cs` 15-18

cs 复制代码
s_msgGCMemory = UnsafeNativeMethods.RegisterWindowMessage("HwndWrapper.GetGCMemMessage");

这两处也会用到 atom table,但它们都是 static 初始化,只会发生一次。

真正符合"持续上涨"的,是 `HwndWrapper` 实例构造时的:

  • `Guid.NewGuid()` 生成唯一类名;

  • `RegisterClassEx` 持续注册新窗口类;

  • 每个新 Dispatcher 对应一个新的 `MessageOnlyHwndWrapper`。

四、为什么 `Geometry.Parse` 返回的是冻结对象,但问题仍然存在

文件:`ParsersCommon.cs` 78-86

cs 复制代码
StreamGeometry geometry = new StreamGeometry();
...
geometry.Freeze();
return geometry;

文件:`Freezable.cs` 846-852

cs 复制代码
Freezable_Frozen = true;
this.DetachFromDispatcher();

这说明:

  • 解析完成后,`StreamGeometry` 会被冻结;

  • 冻结后会 `DetachFromDispatcher()`,所以最终返回出去的 Geometry 本身不再保留线程亲和性。

但副作用发生得更早:

  • `new StreamGeometry()` 时,`DispatcherObject` 已经访问了 `Dispatcher.CurrentDispatcher`;

  • 若线程还没有 Dispatcher,就已经 `new Dispatcher()`;

  • `Dispatcher` 构造期间已经创建了 `MessageOnlyHwndWrapper`、注册了窗口类 atom、创建了 message-only HWND。

因此,`Freeze()` 只能让返回对象脱离 Dispatcher,不能撤销前面已经创建出的线程级 WPF 基础设施。

五、为什么会持续上涨

从WPF源码可以推导出一个很重要的情况:

  • 如果始终是"同一个后台线程"反复调用 `Geometry.Parse`,理论上该线程的 Dispatcher 只会首次创建一次,atom 不会按"每次 Parse"线性增长。

  • 如果高频计算链路会不断使用"新的后台线程"首次触碰 WPF,或者线程池线程数量持续扩张/线程频繁轮换,那么每个新线程第一次进入 `Geometry.Parse` 时都会重复上述链路,从而产生新的 Dispatcher 和新的 `HwndWrapper` 类 atom。

六、为什么这些 atom 可能不会立刻回收

  1. Dispatcher 的 message-only window 销毁发生在 shutdown 期间。

文件:`Dispatcher.cs` 1839-1850

cs 复制代码
window = _window;
_window = null;
window.Dispose();
  1. `HwndWrapper.Dispose` 最终会销毁窗口并注销类。

文件:`HwndWrapper.cs` 305-316

cs 复制代码
UnsafeNativeMethods.DestroyWindow(...);
UnregisterClass((object)classAtom);

也就是说,只有当 Dispatcher 走到 shutdown / window dispose,窗口类才会 `UnregisterClass`。

如果后台线程上自动创建的 Dispatcher 并没有显式 shutdown,那么这套 message-only window / class atom 的释放时机就不会很积极,表现出来就是 atom table 节点持续累积。

七、完整执行链路总结

`Geometry.Parse(pathData)`

-> `MS.Internal.Parsers.ParseGeometry(...)`

-> `new StreamGeometry()`

-> `StreamGeometry : Geometry : Animatable : Freezable : DependencyObject : DispatcherObject`

-> `DispatcherObject..ctor()`

-> `Dispatcher.CurrentDispatcher`

-> `Dispatcher.FromThread(Thread.CurrentThread)` 返回 null

-> `new Dispatcher()`

-> `new MessageOnlyHwndWrapper()`

-> `HwndWrapper..ctor(...)`

-> 生成唯一类名 `HwndWrapper[App;Thread;Guid]`

-> `RegisterClassEx` 生成 class atom

-> `CreateWindowEx(..., HWND_MESSAGE, ...)` 创建 message-only hidden window

-> 回到 `ParseGeometry` 继续填充 path 数据

-> `geometry.Freeze()`

-> `Freezable.DetachFromDispatcher()`

-> 返回冻结后的 `StreamGeometry`

八、最终判断

基于 WPF 源码,问题可以明确归因于:

  • `Geometry.Parse` 在后台线程上不是"纯计算",而是"隐式触发 WPF Dispatcher 基础设施初始化";

  • 真正导致 Atom Table 节点增长的是 `Dispatcher` 创建 `MessageOnlyHwndWrapper` 时调用 `RegisterClassEx` 注册唯一窗口类;

  • 因为类名包含 GUID,所以 atom 不是复用,而是新增;

  • `Freeze()` 只解决返回对象的线程亲和性,不会逆转已经发生的 Dispatcher/HwndWrapper 创建。

九、所以对问题的最终解释

"非 UI 线程创建 Geometry 导致原子表节点增加",从源码上是成立的;更准确地说,是:

`非 UI 线程首次创建 WPF Geometry`

=> `DispatcherObject` 触发该线程 `Dispatcher` 自动创建

=> `Dispatcher` 创建 `MessageOnlyHwndWrapper`

=> `HwndWrapper.RegisterClassEx` 注册唯一窗口类

=> Atom Table 节点增长。

因此,监控里看到的 atom table 增长,本质上是后台计算链路把 WPF 的线程/窗口基础设施偷偷带进来了。

相关推荐
蝈理塘(/_\)大怨种3 小时前
c++ 入门基础
开发语言·c++
糖果店的幽灵3 小时前
LangChain 基于 Python 的技术- agent模块使用总结
开发语言·python·langchain
雪度娃娃3 小时前
转向现代C++——优先选用别名声明,而非 typedef
开发语言·c++
沐知全栈开发3 小时前
PHP While 循环
开发语言
Data_Journal4 小时前
什么是数据采购,它究竟如何运作?
大数据·开发语言·数据库·人工智能·python
我是苏苏4 小时前
C#基础:Winform桌面开发中自定义组件UI、属性及事件
服务器·数据库·c#
之歆4 小时前
DAY_14JavaScript DOM 进阶:HTML DOM 接口、事件监听与经典交互实战
开发语言·前端·javascript·html·ecmascript·交互
笨蛋不要掉眼泪4 小时前
Java并发编程:深入理解ThreadLocal
java·开发语言·jvm·并发
番茄去哪了4 小时前
JVM虚拟机(中)
java·开发语言·jvm