引言

在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, ...)` 创建“消息专用窗口”。
4. 因此,Atom Table 中持续上涨的节点,真正持续增长的核心来源不是 `Geometry` 本身,而是“后台线程第一次触碰 WPF 对象 -> 自动创建 Dispatcher -> 创建 `MessageOnlyHwndWrapper` -> 注册唯一窗口类”的副作用。
5. `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

public static Geometry Parse(string source)
{
    IFormatProvider formatProvider = TypeConverterHelper.InvariantEnglishUS;
    return MS.Internal.Parsers.ParseGeometry(source, formatProvider);
}

2. `ParseGeometry` 创建 `StreamGeometry`
文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\ParsersCommon.cs`
关键位置:73-86

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 对象创建链路。

3. `StreamGeometry` 属于 WPF Freezable/DispatcherObject 体系
文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\StreamGeometry.cs`
关键位置:20

public sealed partial class StreamGeometry : Geometry

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\Generated\Geometry.cs`
关键位置:30

public abstract partial class Geometry : Animatable

文件:`src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Media\Animation\Animatable.cs`
关键位置:13

public abstract partial class Animatable : Freezable

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Freezable.cs`
关键位置:18

public abstract class Freezable : DependencyObject

文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs`
关键位置:41

public class DependencyObject : DispatcherObject

4. 真正触发线程绑定的是 `DispatcherObject` 构造函数
文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\DispatcherObject.cs`
关键位置:121-124

protected DispatcherObject()
{
    _dispatcher = Dispatcher.CurrentDispatcher;
}

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

5. `CurrentDispatcher` 会在当前线程没有 Dispatcher 时自动创建
文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\Dispatcher.cs`
关键位置:44-58

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

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

6. `Dispatcher` 构造函数会创建消息专用窗口
文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\Dispatcher.cs`
关键位置:1717-1740

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 不是轻量标记对象,它会直接创建一个隐藏的消息窗口。

7. `MessageOnlyHwndWrapper` 明确就是 message-only window
文件:`src\Microsoft.DotNet.Wpf\src\Shared\MS\Win32\MessageOnlyHwndWrapper.cs`
关键位置:8-10

internal class MessageOnlyHwndWrapper : HwndWrapper
{
    public MessageOnlyHwndWrapper() : base(0, 0, 0, 0, 0, 0, 0, "", NativeMethods.HWND_MESSAGE, null)

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

8. `HwndWrapper` 为什么会制造 Atom Table 节点
文件:`src\Microsoft.DotNet.Wpf\src\Shared\MS\Win32\HwndWrapper.cs`
关键位置:88-116

_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

_msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");

2. `HwndWrapper` static ctor 里还有:
文件:`HwndWrapper.cs` 15-18

s_msgGCMemory = UnsafeNativeMethods.RegisterWindowMessage("HwndWrapper.GetGCMemMessage");

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

真正符合“持续上涨”的,是 `HwndWrapper` 实例构造时的:
- `Guid.NewGuid()` 生成唯一类名;
- `RegisterClassEx` 持续注册新窗口类;
- 每个新 Dispatcher 对应一个新的 `MessageOnlyHwndWrapper`。

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

文件:`ParsersCommon.cs` 78-86

StreamGeometry geometry = new StreamGeometry();
...
geometry.Freeze();
return geometry;

文件:`Freezable.cs` 846-852

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

window = _window;
_window = null;
window.Dispose();

2. `HwndWrapper.Dispose` 最终会销毁窗口并注销类。
文件:`HwndWrapper.cs` 305-316

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 的线程/窗口基础设施偷偷带进来了。


 

更多推荐