5

Why the value of an AsyncLocal field is not preserved when is set from an asynchronous method of a class. Consider this example:

var scope = new TestScope();

// The default value is 0
Console.WriteLine(scope.Counter.Value);

// Setting the vlaue to 2
await scope.SetValueAsync();

Console.WriteLine(scope.Counter.Value);

class TestScope
{
    public readonly AsyncLocal<int> Counter = new AsyncLocal<int> { Value = 0 };

    public async Task SetValueAsync()
    {
        this.Counter.Value = 2;

        await Task.Yield();
    }
}

The expected output should be:

0

2

But the actual is:

0

0

Why the async context is changed when exiting the SetValueAsync method?

Khodaie
  • 346
  • 4
  • 17
  • See J. Andrew Laughlin's answer [here](https://stackoverflow.com/questions/49232514/why-does-asynclocalt-return-different-results-when-code-is-refactored-slightly) – Klaus Gütter Nov 30 '20 at 08:59
  • Yes. I found out the reason in the post. But, is there any solution to wrap an `AsyncLocal` field in a class? – Khodaie Nov 30 '20 at 10:19
  • `AsyncLocal` is for passing values *down*. It's not intended to *return* values *up*. While you can hack in a mutable wrapper, I wouldn't recommend it. That could easily cause other sharing problems in more complex scenarios. It's much better to only use `AsyncLocal` to pass values down. – Stephen Cleary Nov 30 '20 at 14:48

1 Answers1

12

That's because AsyncLocal has copy-on-write semantics. Basically, when changed from an inner async scope, a copy of the value is made and the original object isn't modified.

You can get around this by wrapping your value in a reference object. That's because even if you make a copy of a reference, you're still manipulating the same object in the end:


class TestScope
{
    public readonly AsyncLocal<StrongBox<int>> Counter = new AsyncLocal<StrongBox<int>> { Value = new StrongBox<int>(0) };

    public int Value
    {
        get => Counter.Value.Value;
        set => Counter.Value.Value = value;
    }

    public async Task SetValueAsync()
    {
        this.Value = 2;

        await Task.Yield();
    }
}

static async Task Test()
{
    var scope = new TestScope();

    // The default value is 0
    Console.WriteLine(scope.Value);

    // Setting the value to 2
    await scope.SetValueAsync();

    Console.WriteLine(scope.Value);
}

StrongBox<T> is just a convenient way to wrap a value type inside of a reference type. But any other reference type would have done the trick.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94