2

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?

chwarr
  • 6,777
  • 1
  • 30
  • 57
Christoph
  • 23
  • 3
  • Related: [1](https://stackoverflow.com/questions/28871878/are-you-there-asynchronously-written-value "Are you there, asynchronously written value?"), [2](https://stackoverflow.com/questions/6581848/memory-barrier-generators "Memory barrier generators") – Theodor Zoulias Nov 11 '20 at 08:52

1 Answers1

5

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks for answering. I feared that there are no documented guarantees. What would you recommend for the example? Doing any changes to the dto object inside a c# lock? And after completion do a lock to be sure, that the dto object has most current value? – Christoph Jul 31 '20 at 16:45
  • 2
    @Christoph: No - I'd recommend just going with the flow and accepting that if we needed to be paranoid about this, *everyone's* async code would just break. Basically there's a *de facto* guarantee, even though it's not a documented one. I would be astonished to see this genuinely not work. (Note that this is just talking about the "serial" case - obviously if you have multiple tasks working in parallel on the same data, you've got all the normal issues.) – Jon Skeet Jul 31 '20 at 16:52
  • And yes, I am only refering to a "serial" async loop like in the example. No parallel modification on the same data. – Christoph Jul 31 '20 at 17:00
  • Thanks again for your very helpful and detailed answer! – Christoph Jul 31 '20 at 17:03