在C#的async/await中,可变对象字段是否需要使用volatile?
问题背景
我查阅了大量相关问题,但遗憾的是未在官方文档中找到明确说明。
示例代码
public class Person { // public byte[] name; // (1) 未加volatile的写法 public volatile byte[] name; // (2) 加volatile的写法 public int nameLength; } public class PersonService { private async Task ReadPerson(Person person) { // byte[] personBytes = File.ReadAllBytes("personPath.txt"); // (3) 同步读取文件 byte[] personBytes = await File.ReadAllBytesAsync("personPath.txt"); // (4) 异步读取文件 person.name = personBytes; person.nameLength = personBytes.Length; } public async Task HandlePerson() { Person person = new Person(); await ReadPerson(person); string personName = System.Text.Encoding.UTF8.GetString(person.name, 0, person.nameLength); Console.WriteLine(personName); } }
代码模式与线程分析
HandlePerson方法会创建一个空的Person实例,调用ReadPerson方法为其填充数据,之后再使用该对象。这种模式常用于复用对象或数组。
根据ReadPerson的实现差异(比如同步读取(3)和异步读取(4)的区别),该方法可能在HandlePerson所在的同一线程执行,也可能被调度到其他线程运行。
原子性与内存可见性问题
需要注意的是,Person.name和Person.nameLength的读写操作都是原子性的。但即便如此,默认情况下仍可能读取到字段的旧状态(比如name为null),这并非async/await特有的问题,而是通用的内存可见性问题。
JIT汇编观察
我查看了这段代码的JIT汇编代码,发现两处不同的lock cmpxchg [ecx], edi指令,这有可能是编译器因async/await自动生成的内存屏障,但也可能和该逻辑无关。此外,由于使用x86架构(相比ARM64有更严格的内存模型保证),C#编译器可能会优化掉部分内存屏障指令。
核心疑问
对于这类可变字段(比如示例中的Person.name,对比(1)和(2)两种写法),是否需要使用volatile修饰?如果需要,适用的条件是什么?
内容的提问来源于stack exchange,提问作者Denis
相关产品推荐
相关产品推荐

