Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 751
Ashwin Avatar asked May 09 '14 01:05

Ashwin


2 Answers

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.
like image 69
Stephen Cleary Avatar answered Sep 28 '22 06:09

Stephen Cleary


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

There is a new feature called

AsyncLocal<T>

https://docs.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

like image 33
Markus Avatar answered Sep 28 '22 07:09

Markus