我看到很多关于这方面的问题(例如https://stackoverflow.com/a/54413147/1756750,或https://stackoverflow.com/a/55139219/1756750),但不幸的是我没有在官方文档中找到任何内容(https://github.com/dotnet/docs/issues/11360)。
让我们考虑以下示例。
public class Person
{
// public byte[] name; // (1)
public volatile byte[] name; // (2)
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(person)
,它以某种方式得到一个person
。在那之后,HandlePerson()
以某种方式使用了这个对象。如果我们想重用对象或数组,可以使用此类代码模式代码。
取决于
ReadPerson(person)
的实现细节(例如,(3)
vs (4)
),这个方法可以在同一个线程上执行(用于HandlePerson()
),或者它可以被重新安排到另一个线程。
下一个观察是
Person.name
和Person.nameLength
都具有原子读/写(例如C#中哪些操作是原子的?)。但是,如果我没记错的话,这并不意味着我们默认不需要volatile
,因为在一般情况下我们仍然可以看到旧状态(null value
)(不特定于异步/等待)。
我还尝试检查 Jit ASM 以获取此代码。我可以看到 2 个不同的
lock cmpxchg [ecx], edi
,这可能是内存障碍,由编译器自动创建,因为 async/await
。但是,它可能与此无关。此外,C# 编译器可以优化一些内存屏障,因为我们使用 x86
,它具有非常严格的内存保证(与 ARM64 相比)。
所以,我的问题是,我们是否需要对可变字段使用
volatile
(例如Person.name
,参见(1)
vs (2)
),如果需要,在什么条件下?
我看到很多关于这方面的问题(例如https://stackoverflow.com/a/54413147/1756750,或https://stackoverflow.com/a/55139219/1756750),但不幸的是我没有在官方文档中找到任何内容(https://github.com/dotnet/docs/issues/11360)。
我有点困惑。你说你已经找到了答案,但没有官方文档。所以......你问SO?即使你得到了答案,它仍然是一个 SO 答案而不是官方文档。
我们是否需要对可变字段使用 volatile
没有。有足够的内存屏障,您不需要
volatile
(或您自己的内存屏障),即使await
之后的代码在不同的线程上恢复。
AFAIK,这实际上并没有正式记录在任何地方,但考虑一个半矛盾的证明:如果这 不是 情况,那么绝大多数
async
代码将是错误的,包括很多BCL 和 MS 框架代码。如果写错 async
代码那么容易,那么就会有很多关于它的文档、抱怨陷阱的文章等等。但这些都不存在,async
代码很可能是正确的。