我是否需要在c#中使用volatile与async/await的可变对象字段?



我看到了很多关于这个领域的问题(例如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.namePerson.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代码很可能是正确的。

相关内容

  • 没有找到相关文章

最新更新