Are object changes inside async await methods visible after completion in any case?

Viewed 369

Are object changes inside async/await methods visible after completion in any case?

After a lot of investigation I still could not find a clear statement if an update made to an outside object from the inside of an async await method is visible to proceeding code after completion of await in any case.

The code example is some kind of a filter chain, in which each filter should see any changes done by previous filters. After that, the same object will be used for further processing.

No parallel execution of filters takes place. In addition to that, I do not want to clone objects due to performance reasons.

Please see following code example:

class Program
{
    private List<ICallback> _allCallbacks = new List<ICallback>();

    public Program()
    {
        // Setup callbacks, perhaps by dependency injection
        _allCallbacks.Add(new MyCallback1());
        _allCallbacks.Add(new MyCallback2());
    }

    static async Task Main()
    {
        Program p = new Program();
        await p.RunBusinessLogic();
        Console.ReadLine();
    }

    private async Task RunBusinessLogic()
    {
        MyDto dto = new MyDto();

        // Setting initial value
        dto.TestProperty = "start";

        // Execute all callbacks and await completion
        await ExecuteCallbacks(dto).ConfigureAwait(false);

        // *** Is dto.TestProperty always guaranteed to be most current value ***?
        Console.WriteLine(dto.TestProperty); // start-1-2?
    }

    public async Task ExecuteCallbacks(MyDto source)
    {
        foreach (ICallback callback in _allCallbacks)
        {
            // Execute Callbacks one after the other, no parallel execution here
            await callback.OnCallback(source).ConfigureAwait(false);
        }
    }

    public class MyDto
    {
        public string TestProperty { get; set; }
    }

    public interface ICallback
    {
        public Task OnCallback(MyDto dto);
    }

    public class MyCallback1 : ICallback
    {
        public async Task OnCallback(MyDto x)
        {
            x.TestProperty += "-1";
            await Task.CompletedTask.ConfigureAwait(false);
        }
    }

    public class MyCallback2 : ICallback
    {
        public async Task OnCallback(MyDto x)
        {
            x.TestProperty += "-2";
            await Task.CompletedTask.ConfigureAwait(false);
        }
    }
}

Possibly related: Do async and await produce acquire and release semantics?

1 Answers

This is a very reasonable concern, and it would be nice if both the C# spec and the CLI spec had a clearer memory model to guarantee this.

No, I don't believe there are any documented by Microsoft guarantees that when a task completes (whether successfully or faulted) and the continuation is executed, that all memory changes executed within the task are visible to the continuation code.

However, I believe the pragmatic reality is that yes, all changes will be visible as if there were a full memory barrier at the point of the task completing. It looks like Joe Albahari asserts that this is the case but I'd love to see a more "from the horse's mouth" guarantee.

There are some other similar aspects to what the JIT compiler is theoretically allowed to do - which is considerably more than the JIT compiler authors would ever think reasonable to do. I'd love to see more detailed guarantees about this over time, but I'm not massively confident about that happening.

Related