Search code examples
c#multithreadingasync-awaitroslynvolatile

Do I need to use volatile for mutable object fields with async/await in C#?


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?


Solution

  • 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 ).

    I'm a little confused. You say that you've found SO answers for this, but no official docs. So... you ask on SO? Even if you get an answer, it's still an SO answer and not official docs.

    do we need to use volatile for mutable fields

    No. There are sufficient memory barriers that you don't need volatile (or your own memory barriers), even if the code after await resumes on a different thread.

    AFAIK, this is not actually documented anywhere officially, but consider a semi-proof-by-contradiction: if this wasn't the case, then the vast, vast majority of async code would be wrong, including lots of BCL and MS framework code. If it was that easy to write wrong async code, then there would be lots of documentation around it, articles complaining about pitfalls, etc. But none of that exists, and it is highly likely that the async code is correct.