ConditionalWeakTable 是线程安全的,但仅限于自身字典操作;其方法内部加锁或使用原子操作,可防止数据结构损坏,适用于 RuntimeHelpers、WPF 附加属性等底层场景,但回调函数需幂等且避免非线程安全操作。
ConditionalWeakTable 的所有公开方法(Add、GetValue、TryGetValue 等)内部都加了锁或使用了原子操作,因此**多线程调用不会导致数据结构损坏或崩溃**。这是它被设计用于 RuntimeHelpers.GetOrCreateObjectData、WPF 的附加属性、EF Core 的实体跟踪等底层场景的根本原因。
但要注意:它的线程安全仅限于自身字典操作。如果你在 createValueCallback 里做非线程安全操作(比如修改共享静态变量、访问未同步的集合),依然会出问题。
GetValue 的回调函数可能被多个线程同时触发——即使键相同,只要尚未完成首次创建,多个线程都可能进入回调GetValue 会阻塞等待;但不同键之间完全无干扰典型场景是给任意对象(比如 Stream 或自定义类实例)附着一个线程本地或生命周期绑定的辅助对象,又不想阻止原对象被回收。这时常配合 GetValue 使用:
var table = new ConditionalWeakTable>(); var resource = table.GetValue(obj, _ => new Lazy (() => new ExpensiveResource()));
上面写法看似简洁,但有隐患:Lazy 的构造本身不执行初始化,而 resource.Value 第一次访问才创建实例——这意味着多个线程仍可能并发进入

new ExpensiveResource(),除非你用 LazyThreadSafetyMode.ExecutionAndPublication(默认就是它)。
lock 去保护外部资源——这容易引发死锁,因为 GetValue 内部已有锁
Lazy 包一层,且显式指定 LazyThreadSafetyMode.ExecutionAndPublication
ConditionalWeakTable 和 ConcurrentDictionary 完全不是一回事:
Keys、Values、GetEnumerator 全部抛 NotSupportedException),后者支持Count 属性,无法知道当前挂了多少关联项;后者有Remove 方法),只能等 key 被回收;后者可主动清理如果你需要“带弱引用语义的并发字典”,ConditionalWeakTable 不满足需求——得自己封装,比如用 ConcurrentDictionary 并定期清理失效引用,但这会失去 ConditionalWeakTable 的自动清理优势。
在 Visual Studio 调试器里,ConditionalWeakTable 的字段(如 m_tables)是私有的、内部结构复杂,且调试器不会触发其弱引用遍历逻辑。所以断点停住后展开对象,大概率看到空的 Keys 或报错“not supported”。
验证是否生效,唯一可靠方式是:
GC.Collect() + GC.WaitForPendingFinalizers() 后检查原 key 是否还能通过 TryGetValue 拿到值它的行为藏在 GC 和运行时协作深处,表面安静,实际很忙。