我看到了很多关于这个领域的问题(例如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
)(不特定于async/await)。
我还尝试检查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
之后的代码在另一个线程上恢复。
async
代码将是错误的,包括许多BCL和MS框架代码。如果很容易编写错误的async
代码,那么就会有很多关于它的文档,抱怨陷阱的文章等等。但这些都不存在,async
代码很可能是正确的。