How to maintain Thread context across async await model in C#?
Asked Answered
P

2

11

Is using ThreadStatic and setting the context every time await completes "an option"? Is there another way?

public async void Test()
{
    // This is in Thread 1
    Foo foo = new Foo();
    Context.context = "context1"; // This is ThreadStatic
    string result = await foo.CallAsynx();

    // This is most likely Thread 2
    Context.context = "context1";   // This might be a different thread and so resetting context    
}

Now is there another way if I don't want to use ThreadStatic?

Pisciform answered 9/5, 2014 at 1:28 Comment(4)
Do you really need thread ThreadStatic? You can flow your global state variables via CallContext.LogicalSetData/LogicalGetData: https://mcmap.net/q/393834/-sharing-scope-across-awaits/1768303Menendez
Or you could change it to foo.CallAsynx(context);. That's the way ASP.NET MVC went.Audition
Besides, async does not create a new thread. I would go with Paulo's aproach. In a ASP.NET context, (Thread)Static is NOT safe, different requests are run on threads from the threadpool, so (thread)static variables will survive and be shared between reqeusts/usersFoggia
I want to avoid ThreadStatic which is the reason for asking this question :). Also, a thing to consider is if the await call throws an exception, the logic I put in would not workPisciform
H
18

ThreadStatic, ThreadLocal<T>, thread data slots, and CallContext.GetData / CallContext.SetData do not work well with async, since they are thread-specific.

The best alternatives are:

  1. Passing it as an argument as @PauloMorgado suggested. Equivalently, you could set it as a field member of an object (it's implicitly passed as an argument via this); or you could have your lambdas capture the variable (underneath, the compiler will implicitly pass it as an argument via this).
  2. Use HttpContext.Items (if you are on ASP.NET 4.5).
  3. Use CallContext.LogicalGetData / CallContext.LogicalSetData as @Noseratio suggested. You can only store immutable data in the logical thread context; and it only works on .NET 4.5 and is not available on all platforms (e.g., Win8).
  4. Force all async continuations back to the same thread by installing a "main loop" for that thread, such as the AsyncContext from my AsyncEx library.
Hemocyte answered 9/5, 2014 at 12:4 Comment(9)
I was experimenting with CallContext.LogicalGetData and CallContext.LogicalSetData and looks like that will work for our scenario. Also, Stephen - I came across your blog on "Implicit Async context". Great blog. Do you know how is LogicalGetData/LogicalSetData designed internally? I hope its not using ThreadStatic :)Pisciform
No, it's not using ThreadStatic. :) The "logical call context" is part of the execution context that is (shallow) copied by the framework as your code "flows" to other threads. In .NET 4.5, the logical call context got copy-on-write behavior, which enables it to work as expected for asynchronous code.Hemocyte
Stephen, re:"and it only works on .NET 4.5 and is not available on all platforms (e.g., Win8)." do you mean WinPho 8?Mandimandible
@Mike: Actually, I was referring to Windows 8 Store Apps. However, I believe this will also not work on Windows Phone.Hemocyte
Will CallContext.LogicalGetData work on ASP.NET Applications running in Azure?Chronologist
@Triynko: It should, as long as you're using .NET 4.5 or above.Hemocyte
Just want to mention here, that since .Net 4.6 there is an AsyncLocal<T> appear to solve this problemSelfpossessed
@StephenCleary, where can I read more about the copy-on-write behavior introduced on .NET 4.5? and thanks for all your great posts!Ermeena
@Ermeena I have an old blog post here. Other than that, your best approach is the source code. :/Hemocyte
T
11

Just if someone has the same question some years later and finds this thread...

There is a new feature called

AsyncLocal<T>

https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=netcore-3.1

This works with "async/await" and also with:

  • Task.Run(...)
  • Dispatcher.BeginInvoke(...)
  • new Thread(...).Start()

I just testet those three with the following code:

    private void StartTests() {
        Thread.Sleep(1000);
        Task.Run(() => DoWork1());
        Task.Run(() => DoWork2());
    }

    private void DoWork1() {
        ThreadContext.Context.Value = "Work 1";
        Thread.Sleep(5);
        Task.Run(() => PrintContext("1"));
        Thread.Sleep(10);
        Dispatcher.BeginInvoke(new Action(() => PrintContext("1")));
        Thread.Sleep(15);
        var t = new Thread(() => PrintContextT("1"));
        t.Start();
    }

    private void DoWork2() {
        ThreadContext.Context.Value = "Work 2";
        Task.Run(() => PrintContext("2"));
        Thread.Sleep(10);
        Dispatcher.BeginInvoke(new Action(() => PrintContext("2")));
        Thread.Sleep(10);
        var t = new Thread(() => PrintContextT("2"));
        t.Start();
    }

    private void PrintContext(string c) {
        var context = ThreadContext.Context.Value;
        Console.WriteLine("P: " + context + "-" + c);

        Task.Run(() => PrintContext2(c));
    }

    private void PrintContext2(string c) {
        Thread.Sleep(7);
        var context = ThreadContext.Context.Value;
        Console.WriteLine("P2: " + context + "-" + c);
    }

    private void PrintContextT(string c) {
        var context = ThreadContext.Context.Value;
        Console.WriteLine("T: " + context + "-" + c);
    }

    public class ThreadContext {
        public static AsyncLocal<object> Context = new AsyncLocal<object>();
    }

Output:

P: Work 2-2

P: Work 1-1

P2: Work 2-2

P: Work 2-2

P2: Work 1-1

P: Work 1-1

P2: Work 2-2

T: Work 2-2

P2: Work 1-1

T: Work 1-1

Tandy answered 18/6, 2020 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.