You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程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可能会做这些优化:
    1. 将_v的读取缓存到寄存器中,导致线程重复使用旧值,看不到其他线程更新后的最新值;
    2. 对指令进行重排,把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 02:57:45