16

When I call WrapperAsync AsyncLocalContext.Value returns null. When I run the same code block outside the method, in the Main method, AsyncLocalContext.Value is not null (which is what I would expect).

The functionality is exactly the same yet the results are different. Is this a bug with the Asynclocal class or is there another explanation?

internal class Program
{
    private static readonly AsyncLocal<string> AsyncLocalContext = new AsyncLocal<string>();

    private static void Main()
    {
        const string text = "surprise!";

        WrapperAsync(text).Wait();
        Console.WriteLine("Get is null: " + (AsyncLocalContext.Value == null));
        // AsyncLocalContext.Value is null

        var value = GetValueAsync(text).Result;
        AsyncLocalContext.Value = value;
        Console.WriteLine("Get is null: " + (AsyncLocalContext.Value == null));
        // AsyncLocalContext.Value is not null
        Console.Read();
    }

    private static async Task WrapperAsync(string text)
    {
        var value = await GetValueAsync(text);
        AsyncLocalContext.Value = value;
    }

    private static async Task<string> GetValueAsync(string text)
    {
        await Task.Delay(0);
        return text;
    }
}
Andrew Jocelyn
  • 399
  • 1
  • 3
  • 18
  • In your `Main` method you are calling `GetValueAsync(text).Result`, whilst in your `WrapperAsync` you are awaiting `GetValueAsync(text)`. – Vidmantas Blazevicius Mar 12 '18 at 10:08
  • If I make `Main` async the results are the same. This behaviour came to light in an Azure Function with async `Run` method. – Andrew Jocelyn Mar 12 '18 at 10:10
  • yes, i'm getting the same results and trying to understand why... with ThreadLocal it works... of course. – Marco Mar 12 '18 at 10:34
  • I don't see why it should behave differently. Can you explain why you expect what you expect? – Evk Mar 12 '18 at 10:40
  • @AndrewJocelyn this is really the expected behavior. See the example in the docs. I however recommend not using AsyncLocal. Exposing a method-level dependency is a better pattern... – Marco Mar 12 '18 at 10:55
  • I don't have a choice. I need context to be picked up by other code which doesn't allow the data to be passed as a parameter. – Andrew Jocelyn Mar 12 '18 at 11:04

3 Answers3

35

AsyncLocal<T> is ambient data stored on the ExecutionContext of the current thread. ExecutionContext is flowed across threads automagically in async/await call chains (see Stephen Toub's blog for details). When the app starts, the default ExecutionContext is used, but once data is stored via AsyncLocal<T>.Value, a new ExecutionContext is created for the current async call chain (see here) and the ambient data is added to it. This new context is propagated to downstream calls.

Stephen Cleary discusses this behavior here (scroll down to the AsyncLocal section) and makes the point:

[AsyncLocal] provides a way for contextual information to flow “down” asynchronous calls. Note that the value does not flow “up”.

This is why AsyncLocal<T> updates down the call chain are not reflected in upstream methods.

J. Andrew Laughlin
  • 1,926
  • 3
  • 21
  • 33
21

Follow this link AsyncLocal Class on MSDN

AsyncLocal<T> represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method

It means that your code uses different values when it's accesses from another async method such as WrapperAsync and your main thread contains another value

[UPDATE]
Not obvious thing to understand, but here is explanation. Control Flow in Async Programs. This is how your thread is changed when you do not expect this.

This is how Control Flow working with async

public class Program
{
    private static readonly AsyncLocal<string> AsyncLocalContext = new AsyncLocal<string>();

    public static void Main(string[] args)
    {
        AsyncLocalContext.Value = "No surprise";
        WrapperAsync("surprise!");
        Console.WriteLine("Main: " + AsyncLocalContext.Value);
    }

    private static async void WrapperAsync(string text)
    {
        Console.WriteLine("WrapperAsync before: " + AsyncLocalContext.Value);
        AsyncLocalContext.Value = text;
        Console.WriteLine("WrapperAsync after: " + AsyncLocalContext.Value);
    }
}

Output is:

WrapperAsync before: No surprise
WrapperAsync after: surprise!
Main: No surprise

[/UPDATE]

Alexey Klipilin
  • 1,866
  • 13
  • 29
  • 4
    The MSDN explanation is incredibly vague. A more robust explanation with examples would be helpful. – Andrew Jocelyn Mar 12 '18 at 10:36
  • nope, this is not what happens. I've tried to output the thread id and it's always the same on his example. It must be something else. – Marco Mar 12 '18 at 10:40
  • I did the same `Console.WriteLine(Thread.CurrentThread.ManagedThreadId)`. Spattered this everywhere and the ID is always the same. – Andrew Jocelyn Mar 12 '18 at 10:41
  • @AndrewJocelyn if you move `AsyncLocalContext.Value = value;` before `await`, what will you get? – Alexey Klipilin Mar 12 '18 at 10:43
  • Actually, if I add some delay in the `GetValueAsync` method, the TheadID changes after `await`. Results are the same. – Andrew Jocelyn Mar 12 '18 at 10:46
  • always the same. But still, you are right. It's context anbient data, and that's why it's not changing. ThreadLocal works because the thread in this case is not changing. So, it's the expected behavior. – Marco Mar 12 '18 at 10:47
  • i've tried with LinqPad, there the id is not changing - still, this is really the expected behavior... – Marco Mar 12 '18 at 10:48
  • @Alexey so it's not about threads but where you set the value in your async code? – Andrew Jocelyn Mar 12 '18 at 11:18
  • @AndrewJocelyn `AsyncLocalContext.Value = text;` – Alexey Klipilin Mar 12 '18 at 11:40
  • 1
    @Marco sorry, missed you comment. Thread ID is the same, I was confused with this due to `Task.Delay(0)`(not at least 1) which does not change thread. Anyway `AsyncLocal` is changed in two cases: in async functions and during changing thread context – Alexey Klipilin Mar 14 '18 at 09:12
2

I found a workaround, and that is to use a Class instead of a scalar variable. This allows the pointer to flow to the child, but doesn't need to flow back up to the parent in order for the parent to see changes from the child.

internal class Program
{
    //class to store in AsyncLocal
    private class HoldData
    {
        public string Text { get; set; }
    }
    private static readonly AsyncLocal<HoldData> AsyncLocalContext = new AsyncLocal<HoldData>();

    private static void Main()
    {
        AsyncLocalContext.Value = new HoldData(); //this sets up the class to hold the data... then the child/parent use the same reference pointer
        const string text = "surprise!";

        WrapperAsync(text).Wait();
        Console.WriteLine("Get is null: " + (AsyncLocalContext.Value.Text == null));
        // AsyncLocalContext.Value.Text is not null

        var value = GetValueAsync(text).Result;
        AsyncLocalContext.Value.Text = value;
        Console.WriteLine("Get is null: " + (AsyncLocalContext.Value.Text == null));
        // AsyncLocalContext.Value.Text is not null
        Console.Read();
    }

    private static async Task WrapperAsync(string text)
    {
        var value = await GetValueAsync(text);
        AsyncLocalContext.Value.Text = value;
    }

    private static async Task<string> GetValueAsync(string text)
    {
        await Task.Delay(0);
        return text;
    }
}
Brain2000
  • 4,655
  • 2
  • 27
  • 35
  • 1
    You can get away with this in a simple case like the one posted, but generally speaking, storing mutable state inside `AsyncLocal` is a bad idea. Imagine someone downstream called `Task.Run` to start a separate task. Now you have two concurrently running tasks that share the same, mutable state. – Peter Ruderman Aug 31 '22 at 20:56
  • @PeterRuderman correct, but it's good to know the rudimentary details on how this all works and what you can and cannot do. Just another tool in the toolbox... – Brain2000 Sep 03 '22 at 02:36