伪共享是cpu缓存行机制导致的性能问题:多线程修改逻辑独立但物理同属一个64字节缓存行的变量时,因mesi协议频繁失效重载缓存行,引发性能断崖式下降。

伪共享不是代码写错了,而是 CPU 缓存机制在多线程场景下“误判”了竞争关系——两个线程各自修改完全无关的变量,却因它们物理上挤在同一缓存行里,被迫反复同步、失效、重载,性能断崖式下跌。
缓存行才是 CPU 真正打交道的单位
CPU 从不单独读写一个 int 或 long。它每次访问内存,都按整块 64 字节(主流 x86-64 架构)加载进 L1 缓存,这个块就是缓存行(Cache Line)。哪怕你只改第 1 个字节,整个 64 字节都会被标记为“已修改”,并通知其他核心:你们那份副本作废了。
这意味着:
- 变量在源码里逻辑独立,但在内存布局中若落在同一缓存行,就自动绑定成“命运共同体”
- 线程 A 写字段 x → 线程 B 的缓存行失效 → B 下次读 y 就得重新从 L2 或主存拉整行 → 延迟飙升
- MESI 协议在这种高频无效化下持续开销,L1 命中率骤降,总线流量激增
C# 中伪共享最常出现的几个地方
.NET 程序跑在 JIT 编译后的机器码上,内存布局由运行时决定,但并不天然规避硬件约束。以下结构极易踩坑:
-
计数器数组:如
long[] counters = new long[8],相邻元素紧挨着,8 个 long(64 字节)刚好塞满一行 → 多个线程往不同索引写,全在抢同一行 -
状态标志组:多个
volatile bool或int字段连续定义在类或结构体中,尤其用于线程控制信号时 - 高性能环形缓冲区:生产者/消费者各自维护 head/tail 指针,若这两个字段未隔离,一写就拖垮对方缓存
在 C# 里真正管用的三种隔离方式
核心目标明确:让会被不同线程独占写入的字段,彼此间隔 ≥64 字节。C# 没有 alignas,但有更可控的替代方案:
-
结构体 + 手动填充字段:定义含
[StructLayout(LayoutKind.Explicit)]的结构,用[FieldOffset]精确指定关键字段位置,中间插入long p1, p2, p3, p4;等填充字段确保间隔 -
类字段 + [StructLayout] + 填充字段:对需要隔离的字段所在类启用显式布局,并在字段间加入无用但占空间的
private byte[56]类型填充(64 − 字段大小) -
利用 System.Runtime.Intrinsics.X86:通过
CacheLineSize属性(.NET 5+)动态获取当前平台缓存行大小,配合Unsafe.AddByteOffset做运行时对齐计算,适合通用容器库
怎么知道你中招了?别靠猜,看指标
伪共享没有异常,只有“莫名其妙变慢”。典型信号包括:
- 增加线程数或 CPU 核数后,吞吐量不升反降,甚至 2 核比 1 核还慢
- perf(Linux)或 VTune(Windows)显示高比例的
L2_RQSTS.RETRY或MEM_LOAD_RETIRED.L1_MISS - 使用 dotTrace / dotMemory 观察到某组字段读写延迟异常抖动,且对应线程 CPU 使用率虚高










