多线程CAS计数器中非volatile long变量的取值正确性问题及解决方案咨询
关于Counter类中long类型读取的可见性与原子性问题
这个问题抓得非常准,确实存在潜在风险,咱们来一步步分析清楚:
为什么v0 = _v可能出问题?
这里有两个核心隐患:
- 32位系统上的原子性问题:long是64位数据类型,在32位CLR环境中,读取long值需要分两次32位操作完成。如果其他线程在这两次操作之间修改了
_v,就会导致v0读到一个“撕裂”的数值——高32位是旧值,低32位是新值,完全不符合预期。 - 可见性与编译器优化问题:即使是64位系统,因为
_v无法被标记为volatile(C#的volatile关键字确实不支持long/ulong类型),编译器、JIT或者CPU可能会做这些优化:- 将
_v的读取缓存到寄存器中,导致线程重复使用旧值,看不到其他线程更新后的最新值; - 对指令进行重排,把
v0 = _v的操作移到循环外部,直接导致CAS循环一直用同一个旧值重试,永远无法退出。
- 将
怎么解决这个问题?
我们需要保证每次读取_v时,既能拿到主内存中的最新值,又能在32位系统上保证原子性。有两种可靠的方案:
方案1:使用Volatile.Read<T>
Volatile.Read是通用的内存屏障工具,支持包括long在内的所有值类型,它会插入必要的内存屏障,禁止编译器优化读取操作,同时在32位系统上通过内部机制保证64位值的原子读取。
修改后的代码如下:
sealed class Counter { private long _v = long.MinValue; public bool GetNextNumber(out ulong output) { long v0, v1; ulong max = ulong.MaxValue - (ulong)1; output = 0; do { v0 = Volatile.Read(ref _v); // 替换直接读取为Volatile.Read if (unchecked((ulong)v0 > max)) return false; v1 = v0 + 1; } while (v0 != Interlocked.CompareExchange(ref _v, v1, v0)); output = unchecked((ulong)v0); return true; } }
方案2:使用Interlocked.Read
如果只针对long类型,Interlocked.Read是更专用的选择,它的作用和Volatile.Read一致,专门用于long的原子读取和可见性保证,用法也很简单:
v0 = Interlocked.Read(ref _v);
补充说明
可能你会疑惑:Interlocked.CompareExchange本身不是带有内存屏障吗?为什么还需要额外处理读取?
没错,CAS操作确实会产生全内存屏障,但这个屏障只保证CAS操作前后的内存同步——也就是说,只有在CAS执行之后,线程的缓存才会刷新。但第一次读取_v的时候,还没有执行CAS,这时候还是可能读到缓存里的旧值,或者撕裂值。所以必须在读取_v的时候就加上内存屏障,确保每次读取都是可靠的。
内容的提问来源于stack exchange,提问作者shingo
相关产品推荐
相关产品推荐

