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的原子操作逻辑无关,完全是值类型的属性访问语义导致的:
- 代码中
Counter是带get访问器的自动属性,当属性类型是值类型(struct)时,每次通过实例访问该属性,get方法返回的都是存储值的独立拷贝,而非Stats实例中原始字段所在的内存引用。 - 调用
test.Counter.Increment()时的实际执行流程为:- 触发
Counter的get访问器,生成一个存放在线程栈上的临时InterlockedInt64副本 - 对这个临时副本内部的
_value字段调用Interlocked.Increment,副本的值确实完成了+1 - 方法执行结束后临时副本随栈帧回收,
test实例中存储的原始Counter值从未被修改,因此每次读取计数都是初始值0
- 触发
- 直接使用
long字段可正常工作的原因:访问类的公共字段时,会直接拿到字段在实例内存中的真实地址,Interlocked.Increment(ref 字段)直接修改原内存位置的值,不存在拷贝环节。 - class版本可正常工作的原因:引用类型的get访问器返回的是对象引用的拷贝,该拷贝和原始引用指向堆上同一个
InterlockedInt64实例,调用Increment修改的是堆上实例的内部值,修改可以被持久感知。
可以通过一个简单改动验证该逻辑:将Stats中的Counter从自动属性改为公共字段,即使InterlockedInt64是struct类型,计数更新也会正常生效——因为字段访问不会生成值拷贝。
内容的提问来源于stack exchange,提问作者tengl
相关产品推荐
相关产品推荐

