.NET ConcurrentDictionary
.NET ConcurrentDictionary<TKey, TValue> 实现原理详解
1. 概述
ConcurrentDictionary<TKey, TValue> 是 .NET 中的线程安全字典实现,专门设计用于高并发场景。它采用了 分段锁(Segment Locking) 技术,通过细粒度锁来实现线程安全,同时保持高性能。
1.1 设计目标
- 线程安全:所有公共方法都是线程安全的
- 高并发性能:支持多个线程同时读写不同的段
- 可扩展性:能够根据负载自动调整大小和锁的数量
- lock-free 读取:读操作无需获取锁,提供最佳性能
2. 核心数据结构
2.1 主要字段
public class ConcurrentDictionary<TKey, TValue> where TKey : notnull
{
private volatile Tables _tables; // 内部表结构
private int _budget; // 每个锁保护的元素数量上限
private readonly bool _growLockArray; // 是否动态增加锁的数量
private readonly bool _comparerIsDefaultForClasses; // 是否使用默认比较器
private readonly int _initialCapacity; // 初始容量
}
2.2 Tables 内部类
private sealed class Tables
{
internal readonly IEqualityComparer<TKey>? _comparer; // 键比较器
internal readonly VolatileNode[] _buckets; // 哈希桶数组
internal readonly ulong _fastModBucketsMultiplier; // 64位快速取模乘数
internal readonly object[] _locks; // 锁数组(分段锁)
internal readonly int[] _countPerLock; // 每个锁保护的元素计数
}
2.3 节点结构
// VolatileNode 包装器,避免性能问题
private struct VolatileNode
{
internal volatile Node? _node;
}
// 实际的链表节点
private sealed class Node
{
internal readonly TKey _key; // 键
internal TValue _value; // 值
internal volatile Node? _next; // 链表下一个节点
internal readonly int _hashcode; // 缓存的哈希码
}
3. 分段锁机制
3.1 锁分段策略
ConcurrentDictionary 将哈希表分成多个段,每个段由一个锁保护:
// 计算桶和锁的索引
private static ref Node? GetBucketAndLock(Tables tables, int hashcode, out uint lockNo)
{
VolatileNode[] buckets = tables._buckets;
uint bucketNo;
if (IntPtr.Size == 8)
{
bucketNo = HashHelpers.FastMod((uint)hashcode, (uint)buckets.Length, tables._fastModBucketsMultiplier);
}
else
{
bucketNo = (uint)hashcode % (uint)buckets.Length;
}
lockNo = bucketNo % (uint)tables._locks.Length;
return ref buckets[bucketNo]._node;
}
3.2 锁的分配策略
- 默认并发级别:
Environment.ProcessorCount(处理器核心数) - 最大锁数量:1024(MaxLockNumber)
- 动态扩展:可以根据需要增加锁的数量
3.3 锁的获取顺序
// 获取所有锁时必须按索引顺序,避免死锁
private void AcquireAllLocks(ref int locksAcquired)
{
// 首先获取锁0,然后按顺序获取其余锁
AcquireFirstLock(ref locksAcquired);
AcquirePostFirstLock(_tables, ref locksAcquired);
}
4. 核心操作实现
4.1 无锁读取 (TryGetValue)
读操作是完全无锁的,利用了内存模型的保证:
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
Tables tables = _tables; // 读取volatile字段
IEqualityComparer<TKey>? comparer = tables._comparer;
if (typeof(TKey).IsValueType && comparer is null)
{
int hashcode = key.GetHashCode();
for (Node? n = GetBucket(tables, hashcode); n is not null; n = n._next)
{
if (hashcode == n._hashcode && EqualityComparer<TKey>.Default.Equals(n._key, key))
{
value = n._value;
return true;
}
}
}
// ... 引用类型的处理逻辑
}
无锁读取的原理:
- Volatile 读取:
_tables字段是 volatile,确保读取到最新的表引用 - Node 的不变性:Node 的
_key、_hashcode字段是只读的 - 链表的原子性:
_next指针的更新是原子的 - 内存屏障:volatile 字段提供了必要的内存屏障
4.2 写操作 (TryAddInternal)
写操作需要获取相应段的锁:
private bool TryAddInternal(Tables tables, TKey key, int? nullableHashcode, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue)
{
int hashcode = nullableHashcode ?? GetHashCode(comparer, key);
while (true)
{
object[] locks = tables._locks;
ref Node? bucket = ref GetBucketAndLock(tables, hashcode, out uint lockNo);
bool lockTaken = false;
try
{
if (acquireLock)
{
Monitor.Enter(locks[lockNo], ref lockTaken);
}
// 检查表是否被重新调整大小
if (tables != _tables)
{
tables = _tables;
continue; // 重试
}
// 查找现有节点
Node? prev = null;
for (Node? node = bucket; node is not null; node = node._next)
{
if (hashcode == node._hashcode && NodeEqualsKey(comparer, node, key))
{
// 找到现有键
if (updateIfExists)
{
// 原子更新或创建新节点
if (!typeof(TValue).IsValueType || ConcurrentDictionaryTypeProps<TValue>.IsWriteAtomic)
{
node._value = value; // 原子写入
}
else
{
// 创建新节点以保证原子性
var newNode = new Node(node._key, value, hashcode, node._next);
if (prev is null)
{
Volatile.Write(ref bucket, newNode);
}
else
{
prev._next = newNode;
}
}
}
return false; // 键已存在
}
prev = node;
}
// 插入新节点到链表头部
var resultNode = new Node(key, value, hashcode, bucket);
Volatile.Write(ref bucket, resultNode);
tables._countPerLock[lockNo]++;
// 检查是否需要扩容
if (tables._countPerLock[lockNo] > _budget)
{
resizeDesired = true;
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(locks[lockNo]);
}
}
// 如果需要,执行扩容
if (resizeDesired)
{
GrowTable(tables, resizeDesired: true, forceRehashIfNonRandomized: false);
}
return true;
}
}
4.3 删除操作 (TryRemove)
删除操作也需要获取相应段的锁,并小心地维护链表结构。
5. 动态扩容机制
5.1 扩容触发条件
// 当某个段的元素数量超过预算时触发扩容
if (tables._countPerLock[lockNo] > _budget)
{
resizeDesired = true;
}
5.2 扩容过程
private void GrowTable(Tables tables, bool resizeDesired, bool forceRehashIfNonRandomized)
{
int locksAcquired = 0;
try
{
// 获取第一个锁(锁0)
AcquireFirstLock(ref locksAcquired);
// 检查表是否已被其他线程调整大小
if (tables != _tables)
{
return; // 已被调整,退出
}
int newLength = tables._buckets.Length;
if (resizeDesired)
{
// 检查负载因子,如果太低则增加预算而不是扩容
if (GetCountNoLocks() < tables._buckets.Length / 4)
{
_budget = 2 * _budget;
return;
}
// 计算新的表大小(至少是原来的2倍)
newLength = HashHelpers.GetPrime(tables._buckets.Length * 2);
}
object[] newLocks = tables._locks;
// 如果需要,增加锁的数量
if (_growLockArray && tables._locks.Length < MaxLockNumber)
{
newLocks = new object[tables._locks.Length * 2];
Array.Copy(tables._locks, newLocks, tables._locks.Length);
for (int i = tables._locks.Length; i < newLocks.Length; i++)
{
newLocks[i] = new object();
}
}
// 创建新的表结构
var newBuckets = new VolatileNode[newLength];
var newCountPerLock = new int[newLocks.Length];
var newTables = new Tables(newBuckets, newLocks, newCountPerLock, tables._comparer);
// 获取所有其他锁
AcquirePostFirstLock(tables, ref locksAcquired);
// 重新哈希所有数据到新表
foreach (VolatileNode bucket in tables._buckets)
{
Node? current = bucket._node;
while (current is not null)
{
Node? next = current._next;
ref Node? newBucket = ref GetBucketAndLock(newTables, current._hashcode, out uint newLockNo);
newBucket = new Node(current._key, current._value, current._hashcode, newBucket);
newCountPerLock[newLockNo]++;
current = next;
}
}
// 调整预算
_budget = Math.Max(1, newBuckets.Length / newLocks.Length);
// 原子地替换表
_tables = newTables;
}
finally
{
ReleaseLocks(locksAcquired);
}
}
6. 内存模型和可见性
6.1 Volatile 字段的使用
private volatile Tables _tables; // 确保表引用的可见性
internal volatile Node? _next; // 确保链表指针的可见性
internal volatile Node? _node; // 确保节点引用的可见性
6.2 原子性保证
值类型的原子写入:
if (!typeof(TValue).IsValueType || ConcurrentDictionaryTypeProps<TValue>.IsWriteAtomic)
{
node._value = value; // 直接原子写入
}
else
{
// 创建新节点保证原子性
var newNode = new Node(node._key, value, hashcode, node._next);
Volatile.Write(ref bucket, newNode);
}
7. 性能优化技术
7.1 FastMod 优化
// 64位系统使用乘法代替除法
if (IntPtr.Size == 8)
{
return buckets[HashHelpers.FastMod((uint)hashcode, (uint)buckets.Length, tables._fastModBucketsMultiplier)]._node;
}
else
{
return buckets[(uint)hashcode % (uint)buckets.Length]._node;
}
7.2 JIT 优化
// 值类型的特殊处理,允许 JIT 内联
if (typeof(TKey).IsValueType && comparer is null)
{
// 使用 EqualityComparer<TKey>.Default.Equals,可被 JIT 内联
}
7.3 锁的复用
// 第一个锁对象复用锁数组本身,节省一次分配
locks[0] = locks; // reuse array as the first lock object
8. 性能特征
8.1 时间复杂度
| 操作 | 平均情况 | 最坏情况 | 并发特征 |
|---|---|---|---|
| TryGetValue | O(1) | O(n) | Lock-free |
| TryAdd | O(1) | O(n) | 细粒度锁 |
| TryRemove | O(1) | O(n) | 细粒度锁 |
| 扩容 | O(n) | O(n) | 全表锁 |
8.2 并发特征
- 读-读并发:完全无冲突
- 读-写并发:读操作不会被写操作阻塞
- 写-写并发:只有操作同一段时才会冲突
- 扩容期间:所有写操作被阻塞,但很少发生
8.3 内存开销
- 节点开销:每个键值对需要额外的 Node 对象
- 锁开销:每个锁对象约 24 字节
- 表开销:VolatileNode 包装器增加少量开销
9. 适用场景
9.1 理想场景
- 读多写少:无锁读取提供最佳性能
- 高并发:多个线程同时访问不同的段
- 动态负载:能够自动调整以适应负载变化
9.2 注意事项
- 扩容成本:扩容时需要获取所有锁,可能造成短暂的全局阻塞
- 内存使用:比普通 Dictionary 有更多的内存开销
- 小数据集:对于小数据集,锁的开销可能超过收益
10. 与其他集合的比较
10.1 vs Dictionary<TKey, TValue>
| 特性 | ConcurrentDictionary | Dictionary |
|---|---|---|
| 线程安全 | ✅ | ❌ |
| 读性能 | 优秀(无锁) | 优秀 |
| 写性能 | 良好(细粒度锁) | 优秀 |
| 内存使用 | 较高 | 较低 |
10.2 vs ReaderWriterLockSlim + Dictionary
| 特性 | ConcurrentDictionary | RWL + Dictionary |
|---|---|---|
| 并发度 | 高(分段锁) | 中(全局锁) |
| 复杂度 | 低(内置) | 高(手动管理) |
| 死锁风险 | 低 | 中 |
11. 最佳实践
11.1 构造函数选择
// 根据预期的并发级别选择合适的构造函数
var dict = new ConcurrentDictionary<string, int>(
concurrencyLevel: Environment.ProcessorCount,
capacity: expectedSize
);
11.2 批量操作
// 避免在循环中频繁调用 TryAdd
// 考虑使用构造函数传入初始数据
var initialData = GetInitialData();
var dict = new ConcurrentDictionary<string, int>(initialData);
11.3 原子操作
// 使用 AddOrUpdate 进行原子的添加或更新
dict.AddOrUpdate(key,
addValue: 1,
updateValueFactory: (k, v) => v + 1);
12. 总结
ConcurrentDictionary<TKey, TValue> 是 .NET 中最成功的并发数据结构之一,它通过以下关键技术实现了优异的性能:
- 分段锁机制:将冲突限制在最小范围内
- 无锁读取:利用内存模型保证读操作的安全性
- 动态调整:根据负载自动调整表大小和锁数量
- 原子性保证:确保所有操作的原子性和一致性
这些设计使得 ConcurrentDictionary 成为高并发应用程序中的理想选择,为开发者提供了简单易用且高性能的线程安全字典解决方案。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19044694
浙公网安备 33010602011771号