跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

c# ConcurrentDictionary 内部是如何实现分段锁的

ConcurrentDictionary的“分段锁”实为无锁+细粒度同步混合策略,依赖volatile、Interlocked和少量SpinWait,仅扩容或CAS失败超10次时才使用_locks数组中的Monitor锁;segment数自.NET 6+起设为4×CPU核心数(≥31),以平衡冲突率与内存开销。 ConcurrentDictionary 的分段锁不是传统意义上的“分段锁” 它压根没用
lock
语句或
Monitor
对每个段加独占锁,而是基于无锁(lock-free)+ 细粒度同步的混合策略。真正起作用的是数组 +
volatile
字段 +
Interlocked
操作 + 少量
SpinWait
,只有在极少数竞争激烈路径(如扩容、首次插入空桶)才会退化为轻量级锁(
Lock
内部用的
Monitor
)。所以别被“分段锁”字面误导——它不靠多个
lock
对象实现并发控制。 segments 数组和 bucket 分布怎么决定线程是否冲突 初始化时,
ConcurrentDictionary
会按构造参数或默认值(如 .NET 6+ 默认 4 * CPU 核心数)创建一个
Node[]
数组,称为 segments(实际字段名是
_buckets
,但逻辑上按 segment 划分)。每个 key 经过两次哈希:
GetHash(key)
得到原始 hash,再对
_buckets.Length
取模得到 segment 索引。只要两个 key 落在不同 segment,它们的读写就完全无竞争;落在同一 segment 后,才进入该 segment 内部的链表/数组结构处理。 segment 数量在构造后固定,不随元素增长而动态增加(扩容是重建整个
_buckets
数组) 同一个 segment 内部不设子锁,所有操作通过 CAS(
Interlocked.CompareExchange
)更新头节点或 next 引用 写操作(
AddOrUpdate
TryRemove
)会先尝试无锁插入,失败则自旋重试,多次失败后才可能触发 segment 级锁(
_locks[segmentIndex]
) _locks 数组只在扩容和极端竞争时才真正起作用
_locks
是一个
object[]
,长度等于
_buckets.Length
,每个元素对应一个 segment 的“后备锁对象”。但它平时几乎不参与同步: 读操作(
TryGetValue
ContainsKey
)全程无锁,仅靠
volatile
读和引用可见性保证 写操作中,99% 场景下靠
Interlocked
原子操作完成节点插入/删除,
_locks[seg]
不会被持有 只有两种情况会真正
lock (_locks[seg])
: 该 segment 正在扩容中 (此时要阻塞写入),或 CAS 自旋超过阈值(默认 10 次)仍失败 也就是说,
_locks
是兜底机制,不是主干同步手段。 C知道 CSDN推出的一款AI技术问答工具 下载 为什么 .NET 6+ 把 segment 数量从 31 改成 4 × CPU count 老版本(.NET Core 2.x / .NET Framework)硬编码 segment 数为质数 31,容易导致哈希分布不均、热点 segment 集中。新版本改为
Math.Max(4 * Environment.ProcessorCount, 31)
,核心目标是让 segment 数与真实并发线程数匹配: segment 太少 → 多个线程挤进同一 segment,CAS 冲突率上升,自旋开销变大 segment 太多 → 内存占用增加(每个 segment 至少一个空链表头),且 cache line false sharing 风险上升 4×CPU count 是经验值:既覆盖常见线程池线程数,又留出余量应对突发并发
internal static int GetDefaultConcurrencyLevel() => Math.Max(4 * Environment.ProcessorCount, 31);
分段设计的关键不在“锁”,而在“隔离哈希空间”。真正难懂的部分是它如何用纯原子操作模拟链表修改——比如插入时怎么保证 head 更新和 next 指针设置的原子性,这依赖于
Node
结构体的不可变性和
Interlocked.CompareExchange
的循环重试逻辑。这些细节一旦写错,就会出现 ABA 问题或内存泄漏,所以别轻易自己仿写。

相关文章