I've seen a lot of questions about this area (e.g. https://stackoverflow.com/a/54413147/1756750 , or https://stackoverflow.com/a/55139219/1756750 ), but unfortunately I haven't found anything in official documentation ( https://github.com/dotnet/docs/issues/11360 ).
Let's consider the following example.
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);
}
}
The method HandlePerson()
creates an empty person
and calls the method ReadPerson(person)
, which somehow gets a person
. After that the HandlePerson()
uses this object somehow. Such code pattern code be used, if we want to reuse object, or arrays.
Depends on implementation details of ReadPerson(person)
(e.g., (3)
vs (4)
), this method could be executed on the same thread (which was used for HandlePerson()
), or it can be rescheduled to another thread.
The next observation is both Person.name
and Person.nameLength
have atomic read/write (e.g. What operations are atomic in C#? ). However, if I am not mistaken, it doesn't mean that we don't need volatile
by default, because we still can see old state (null value
) in general case (not specific for async/await).
I also tried to check Jit ASM for this code. I can see 2 different lock cmpxchg [ecx], edi
, which could be a memory barriers, which were automatically created by the compiler, because of async/await
. However, it could be something not-related to this. Also, C# compiler could optimize some memory barriers, because we use x86
, which has pretty strict memory guaranties (compare to ARM64).
So, my question is, do we need to use volatile
for mutable fields (e.g. Person.name
, see (1)
vs (2)
), and if yes in what conditions?