引言
在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问题原因
-
`Geometry.Parse(string)` 并不是纯字符串解析,它会在解析过程中创建 `StreamGeometry`,而 `StreamGeometry -> Geometry -> Animatable -> Freezable -> DependencyObject -> DispatcherObject` 整条继承链都属于 WPF 对象体系。
-
`DispatcherObject` 的构造函数会直接绑定 `Dispatcher.CurrentDispatcher`。因此,只要某个线程第一次在该线程上创建 WPF `DispatcherObject`,就可能触发该线程的 `Dispatcher` 自动创建。
-
`Dispatcher` 在构造时会创建一个 `MessageOnlyHwndWrapper`,而 `MessageOnlyHwndWrapper` 底层又会走到 `HwndWrapper`,其中会:
-
生成带 GUID 的唯一窗口类名;
-
调用 `RegisterClassEx` 注册窗口类,拿到 `_classAtom`;
-
调用 `CreateWindowEx(..., HWND_MESSAGE, ...)` 创建"消息专用窗口"。
-
因此,Atom Table 中持续上涨的节点,真正持续增长的核心来源不是 `Geometry` 本身,而是"后台线程第一次触碰 WPF 对象 -> 自动创建 Dispatcher -> 创建 `MessageOnlyHwndWrapper` -> 注册唯一窗口类"的副作用。
-
`Geometry.Parse` 返回前确实会把生成的 `StreamGeometry` `Freeze()`,而 `Freeze()` 内部会 `DetachFromDispatcher()`,所以返回对象最终变成 free-threaded;但这只发生在 Dispatcher 已经被创建之后,无法回滚前面已经创建的 Dispatcher / hidden HWND / class atom。
二、源码链路
- `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);
}
- `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 对象创建链路。
- `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
- 真正触发线程绑定的是 `DispatcherObject` 构造函数
文件:`src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Threading\DispatcherObject.cs`
关键位置:121-124
cs
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
这说明:只要在某线程上创建 `StreamGeometry`,最终都会执行到这里,从而访问 `Dispatcher.CurrentDispatcher`。
- `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()`。
- `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 不是轻量标记对象,它会直接创建一个隐藏的消息窗口。
- `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 相关资源。
- `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 在涨,而不是别的
- `Dispatcher` static ctor 里还有:
文件:`Dispatcher.cs` 22-31
cs
_msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");
- `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 可能不会立刻回收
- Dispatcher 的 message-only window 销毁发生在 shutdown 期间。
文件:`Dispatcher.cs` 1839-1850
cs
window = _window;
_window = null;
window.Dispose();
- `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 的线程/窗口基础设施偷偷带进来了。