c# ConditionalWeakTable 在并发场景下的应用

来源:这里教程网 时间:2026-02-21 17:39:15 作者:

ConditionalWeakTable 是线程安全的,但不等于“自动解决所有并发问题”

ConditionalWeakTable<tkey tvalue></tkey>
的所有公开方法(
Add
GetValue
TryGetValue
等)内部都加了锁或使用了原子操作,因此**多线程调用不会导致数据结构损坏或崩溃**。这是它被设计用于
RuntimeHelpers.GetOrCreateObjectData
、WPF 的附加属性、EF Core 的实体跟踪等底层场景的根本原因。

但要注意:它的线程安全仅限于自身字典操作。如果你在

createValueCallback
里做非线程安全操作(比如修改共享静态变量、访问未同步的集合),依然会出问题。

GetValue
的回调函数可能被多个线程同时触发——即使键相同,只要尚未完成首次创建,多个线程都可能进入回调
回调执行期间,其他线程对同一键的
GetValue
会阻塞等待;但不同键之间完全无干扰
没有内置的“只让第一个线程执行回调,其余等待结果”的去重逻辑——它靠内部锁实现串行化,不是靠 CAS 或 double-check

用 GetValue 创建单例式附加状态时,回调必须幂等

典型场景是给任意对象(比如

Stream
或自定义类实例)附着一个线程本地或生命周期绑定的辅助对象,又不想阻止原对象被回收。这时常配合
GetValue
使用:

var table = new ConditionalWeakTable<MyClass, Lazy<ExpensiveResource>>();
var resource = table.GetValue(obj, _ => new Lazy<ExpensiveResource>(() => new ExpensiveResource()));

上面写法看似简洁,但有隐患:

Lazy<expensiveresource></expensiveresource>
的构造本身不执行初始化,而
resource.Value
第一次访问才创建实例——这意味着多个线程仍可能并发进入
new ExpensiveResource()
,除非你用
LazyThreadSafetyMode.ExecutionAndPublication
(默认就是它)。

更稳妥的做法是直接在回调里返回已构建好的对象,或者确保构造逻辑本身可重入 不要在回调中调用
lock
去保护外部资源——这容易引发死锁,因为
GetValue
内部已有锁
若需延迟初始化 + 线程安全,优先用
Lazy<t></t>
包一层,且显式指定
LazyThreadSafetyMode.ExecutionAndPublication

别把它当 ConcurrentDictionary 用

ConditionalWeakTable
ConcurrentDictionary
完全不是一回事:

前者 key 是弱引用(key 对象被 GC 后,对应条目自动消失),后者是强引用 前者不支持枚举(
Keys
Values
GetEnumerator
全部抛
NotSupportedException
),后者支持
前者没有
Count
属性,无法知道当前挂了多少关联项;后者有
前者不能手动删除(没
Remove
方法),只能等 key 被回收;后者可主动清理

如果你需要“带弱引用语义的并发字典”,

ConditionalWeakTable
不满足需求——得自己封装,比如用
ConcurrentDictionary<weakreference>, TValue></weakreference>
并定期清理失效引用,但这会失去
ConditionalWeakTable
的自动清理优势。

调试时看不到内容,别以为没生效

在 Visual Studio 调试器里,

ConditionalWeakTable
的字段(如
m_tables
)是私有的、内部结构复杂,且调试器不会触发其弱引用遍历逻辑。所以断点停住后展开对象,大概率看到空的
Keys
或报错“not supported”。

验证是否生效,唯一可靠方式是:

写单元测试,用
GC.Collect()
+
GC.WaitForPendingFinalizers()
后检查原 key 是否还能通过
TryGetValue
拿到值
在回调里打日志,确认是否被调用、调用次数是否符合预期(尤其高并发下) 用内存分析工具(如 dotMemory)查看是否存在意外的强引用链阻止 key 回收

它的行为藏在 GC 和运行时协作深处,表面安静,实际很忙。

相关推荐