10

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?

mghie
  • 32,028
  • 6
  • 87
  • 129
Ashwin
  • 121
  • 1
  • 4
  • 1
    Do you really need thread `ThreadStatic`? You can flow your global state variables via `CallContext.LogicalSetData`/`LogicalGetData`: http://stackoverflow.com/q/22363830/1768303 – noseratio May 09 '14 at 07:05
  • 1
    Or you could change it to `foo.CallAsynx(context);`. That's the way ASP.NET MVC went. – Paulo Morgado May 09 '14 at 08:07
  • 1
    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/users – MarkO May 09 '14 at 13:06
  • 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 work – Ashwin May 14 '14 at 21:05

2 Answers2

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.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 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 :) – Ashwin May 15 '14 at 17:38
  • 2
    No, it's not using `ThreadStatic`. :) The "logical call context" is [part of the execution context](http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx) 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. – Stephen Cleary May 15 '14 at 18:54
  • 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? – Mike Dec 04 '14 at 05:45
  • @Mike: Actually, I was referring to Windows 8 Store Apps. However, I believe this will also not work on Windows Phone. – Stephen Cleary Dec 04 '14 at 10:27
  • Will CallContext.LogicalGetData work on ASP.NET Applications running in Azure? – Triynko Jul 27 '15 at 12:16
  • @Triynko: It should, as long as you're using .NET 4.5 or above. – Stephen Cleary Jul 27 '15 at 12:22
  • 11
    Just want to mention here, that since .Net 4.6 there is an `AsyncLocal` appear to solve this problem – AlfeG Jun 09 '16 at 14:15
  • @StephenCleary, where can I read more about the copy-on-write behavior introduced on .NET 4.5? and thanks for all your great posts! – Tamir Aug 01 '17 at 15:52
  • @Tamir I have an [old blog post here](https://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html). Other than that, your best approach is [the source code](http://referencesource.microsoft.com/#mscorlib/system/threading/executioncontext.cs,968). :/ – Stephen Cleary Aug 01 '17 at 16:15
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

Markus
  • 2,184
  • 2
  • 22
  • 32