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

C#中struct与class封装字段的Interlocked更新差异问题

问题背景
  • 现有单例统计类,内置多个计数器,支持在应用任意位置更新计数。最初实现采用public修饰的long/int类型公共字段,通过Interlocked.Increment实现线程安全的计数更新。
  • 后续重构将字段调整为属性,封装了InterlockedInt64类型统一处理原子操作逻辑:
    • 若InterlockedInt64定义为引用类型(class),可通过Clone方法拷贝统计对象快照,用于追踪两次采样间的计数增量
    • 若将其改为值类型(struct),理论上值类型赋值会自动完成拷贝,无需额外实现Clone方法,但实际调用Increment后计数完全无法正常更新
  • 简化复现代码如下:
using System;
using System.Globalization;
using System.Threading;

public class Program
{
    public static void Main()
    {
        var test = new Stats();
        
        test.Counter.Increment();
        Console.WriteLine($"Expected 1, got {test.Counter}");
        test.Counter.Increment();
        Console.WriteLine($"Expected 2, got {test.Counter}");
        test.Counter.Increment();
        Console.WriteLine($"Expected 3, got {test.Counter}");
    }

    public class Stats
    {
        public InterlockedInt64 Counter { get; init; } = new InterlockedInt64();
    }
    
    //public class InterlockedInt64 // 该定义下可正常运行
    public struct InterlockedInt64 // 该定义下计数更新失效
    {
        private long _value;
        public InterlockedInt64()
        {
            _value = 0;
        }

        public InterlockedInt64(long value)
        {
            _value = value;
        }

        public long Increment() => Interlocked.Increment(ref _value);
        public long Decrement() => Interlocked.Decrement(ref _value);
        public long Exchange(long newValue) => Interlocked.Exchange(ref _value, newValue);
        public InterlockedInt64 Clone() => new(_value);
        
        public static implicit operator InterlockedInt64(long v)
        {
            return new InterlockedInt64(v);
        }

        public static implicit operator long (InterlockedInt64 v)
        {
            return v._value;
        }

        public override string ToString() => _value.ToString(CultureInfo.CurrentCulture);
        public string ToString(CultureInfo cultureInfo) => _value.ToString(cultureInfo);
    }
}
问题说明

已了解struct和class的基础差异、class版本正常运行的逻辑,需要明确该现象的底层机制:为什么自定义struct包裹long类型做原子更新会失效,而直接使用long类型字段调用Interlocked.Increment就能正常工作?

编辑说明:已移除部分冗余代码,将原测试类重命名为Stats

底层原因

该问题和Interlocked的原子操作逻辑无关,完全是值类型的属性访问语义导致的:

  1. 代码中Counter是带get访问器的自动属性,当属性类型是值类型(struct)时,每次通过实例访问该属性,get方法返回的都是存储值的独立拷贝,而非Stats实例中原始字段所在的内存引用。
  2. 调用test.Counter.Increment()时的实际执行流程为:
    • 触发Counter的get访问器,生成一个存放在线程栈上的临时InterlockedInt64副本
    • 对这个临时副本内部的_value字段调用Interlocked.Increment,副本的值确实完成了+1
    • 方法执行结束后临时副本随栈帧回收,test实例中存储的原始Counter值从未被修改,因此每次读取计数都是初始值0
  3. 直接使用long字段可正常工作的原因:访问类的公共字段时,会直接拿到字段在实例内存中的真实地址,Interlocked.Increment(ref 字段)直接修改原内存位置的值,不存在拷贝环节。
  4. class版本可正常工作的原因:引用类型的get访问器返回的是对象引用的拷贝,该拷贝和原始引用指向堆上同一个InterlockedInt64实例,调用Increment修改的是堆上实例的内部值,修改可以被持久感知。

可以通过一个简单改动验证该逻辑:将Stats中的Counter从自动属性改为公共字段,即使InterlockedInt64是struct类型,计数更新也会正常生效——因为字段访问不会生成值拷贝。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 18:48:28