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

C#单写多读场景下Decimal线程安全性及锁使用问题

单写多读场景下decimal字段无锁访问风险与方案选型

场景前提

仅存在一个线程循环更新decimal类型的TotalValue属性,其余任意数量线程仅对该属性执行读操作,无锁状态下读线程可能在写线程执行TotalValue = totalCopy赋值的中途访问属性。写线程核心逻辑如下:

class Thread1
{
    public decimal TotalValue {get; private set;}
    private decimal StockAmount;
    private decimal OldPrice;
    public async Task GetStockPrices(string fictionalAsset)
    {
        while (true)
        {
            decimal totalCopy = TotalValue;
            totalCopy -= (StockAmount * OldPrice);
            OldPrice = StockPrices.GetValue(fictionalAsset);
            totalCopy += (StockAmount * OldPrice);
            TotalValue = totalCopy;
        }
    }
}

你原本考虑的普通锁写法如下,担心读多写少场景下锁阻塞带来过高开销:

lock (TotalLock)
{
    TotalValue = totalCopy;
}

问题1:赋值中途读访问的实际后果

当前无锁实现会触发严重业务错误,绝非仅读到旧值这么简单:

  • decimal是16字节的值类型,主流32位、64位CPU的单次原子写入宽度分别为4字节、8字节,因此对decimal的赋值会被拆分为2~4次独立的内存写入操作。如果读线程刚好卡在这几次写入的间隙访问TotalValue,会读到撕裂值(torn read):即一部分字节来自旧值、一部分字节来自新值的拼接结果,这个值完全没有业务意义,可能是0、也可能是和新旧值都无关的随机极大/极小值,属于直接导致计算错误的严重故障,不属于“读到可接受旧版本”的范畴。
  • .NET运行时仅对长度不超过当前平台指针宽度的变量保证原子读写(比如32位环境下的int、64位环境下的long),decimal长度远超这个阈值,不存在天然的无锁安全特性。

问题2:无锁实现的额外缺陷与性能对比

除了上述撕裂读问题,当前无锁实现还存在跨线程可见性缺陷:

  • C#内存模型允许编译器、CPU对没有内存屏障的内存访问做指令重排,也允许线程将变量缓存到CPU私有寄存器或核心私有缓存中,没有同步原语的前提下,读线程可能长时间读不到写线程更新的最新值,甚至永远读到第一次加载的旧值。
  • “无锁性能优于锁”的判断仅在无锁实现本身正确的前提下成立,当前存在正确性问题的无锁实现没有讨论性能的价值。同时你不需要在“错误的无锁实现”和“全互斥的普通lock”之间二选一,读多写少场景下有更适配的高性能方案:
    • 追求极致性能的场景下,可以用Interlocked系列原子方法操作decimal(.NET Core 2.1及以上版本原生支持decimal的Interlocked原子读写),整个读写过程基于CPU级别的原子指令实现,无锁开销,既不会出现撕裂读,也自带全内存屏障保证所有线程能及时读到最新值,读操作完全没有阻塞。
    • 如果需要兼容旧版本.NET,可以使用ReaderWriterLockSlim,该锁实现了读写分离:多个读线程并发访问时完全不会互斥,只有写线程进入时才会短暂阻塞读,性能比读操作也需要抢锁的普通lock高一个数量级以上,不会出现“读访问被持续阻塞”的问题。

问题3:锁的调度规则与写优先级实现

普通lock关键字基于Monitor类实现,调度逻辑如下:

  • 它的等待队列不保证先进先出:整体调度近似公平,但系统允许就绪优先级更高的线程插队,也存在线程被唤醒后锁刚好被其他线程抢占的情况,绝对不要依赖lock的调度顺序实现业务逻辑。
  • 普通lock没有内置的写优先级机制,非常不推荐自己实现“检测锁状态再等待写入”的逻辑——这类自定义自旋等待很容易引发活锁、优先级反转、CPU空转等问题。如果需要写线程优先的调度,直接使用前面提到的ReaderWriterLockSlim即可,它原生支持写优先级策略:当有写线程处于等待队列时,后续新进入的读线程请求会直接排队,优先将锁分配给等待的写线程,完全匹配你的需求。

内容的提问来源于stack exchange,提问作者julian bechtold

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 22:39:19