.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;
            }
        }
    }
    // ... 引用类型的处理逻辑
}

无锁读取的原理:

  1. Volatile 读取_tables 字段是 volatile,确保读取到最新的表引用
  2. Node 的不变性:Node 的 _key_hashcode 字段是只读的
  3. 链表的原子性_next 指针的更新是原子的
  4. 内存屏障: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 成为高并发应用程序中的理想选择,为开发者提供了简单易用且高性能的线程安全字典解决方案。

posted @ 2025-08-18 14:54  MadLongTom  阅读(57)  评论(0)    收藏  举报