using ThreadStatic variables with async/await
Asked Answered
T

5

39

With the new async/await keywords in C#, there are now impacts to the way (and when) you use ThreadStatic data, because the callback delegate is executed on a different thread to one the async operation started on. For instance, the following simple Console app:

[ThreadStatic]
private static string Secret;

static void Main(string[] args)
{
    Start().Wait();
    Console.ReadKey();
}

private static async Task Start()
{
    Secret = "moo moo";
    Console.WriteLine("Started on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine("Secret is [{0}]", Secret);

    await Sleepy();

    Console.WriteLine("Finished on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine("Secret is [{0}]", Secret);
}

private static async Task Sleepy()
{
    Console.WriteLine("Was on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(1000);
    Console.WriteLine("Now on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
}

will output something along the line of:

Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []

I've also experimented with using CallContext.SetData and CallContext.GetData and got the same behaviour.

After reading some related questions and threads:

it seems that frameworks like ASP.Net explicitly migrates the HttpContext across threads, but not the CallContext, so perhaps the same thing is happening here with the use of async and await keywords?

With the use of the async/await keywords in mind, what's the best way to store data associated with a particular thread of execution that can be (automatically!) restored on the callback thread?

Thanks,

Toscana answered 22/10, 2012 at 11:32 Comment(1)
AsyncLocal is the modern way to achieve this now. Mind accepting my answer?Avraham
S
33

You could use CallContext.LogicalSetData and CallContext.LogicalGetData, but I recommend you don't because they don't support any kind of "cloning" when you use simple parallelism (Task.WhenAny / Task.WhenAll).

I opened a UserVoice request for a more complete async-compatible "context", explained in more detail in an MSDN forum post. It does not seem possible to build one ourselves. Jon Skeet has a good blog entry on the subject.

So, I recommend you use argument, lambda closures, or the members of the local instance (this), as Marc described.

And yes, OperationContext.Current is not preserved across awaits.

Update: .NET 4.5 does support Logical[Get|Set]Data in async code. Details on my blog.

Seagoing answered 22/10, 2012 at 12:39 Comment(2)
Can you comment on AsyncLocal<T>? msdn.microsoft.com/en-us/library/dn906268(v=vs.110).aspxCelesta
@b_levitt: AsyncLocal<T> is the modern solution to this problem.Seagoing
A
14

AsyncLocal<T> provides support for maintaining variables scoped to a particular asynchronous code flow.

Changing the variable type to AsyncLocal, e.g.,

private static AsyncLocal<string> Secret = new AsyncLocal<string>();

gives the following, desired output:

Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]
Avraham answered 30/6, 2019 at 13:15 Comment(2)
This answer should now be the correct one, given that it's the most up-to-date and current.Staves
WARNING: AsyncLocal flow downstream, but not upstream. Changing the value in a "child" method will not be reflected in "parent" methodPrivy
S
9

Basically, I would emphasize: don't do that. [ThreadStatic] is never going to play nicely with code that jumps between threads.

But you don't have to. A Task already carries state - in fact, it can do it 2 different ways:

  • there's an explicit state object, which can hold everything you need
  • lambdas/anon-methods can form closures over state

Additionally, the compiler does everything you need here anyway:

private static async Task Start()
{
    string secret = "moo moo";
    Console.WriteLine("Started on thread [{0}]",
        Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine("Secret is [{0}]", secret);

    await Sleepy();

    Console.WriteLine("Finished on thread [{0}]",
        Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine("Secret is [{0}]", secret);
}

No static state; no issues with threads or multiple tasks. It just works. Note that secret is not just a "local" here; the compiler has worked some voodoo, like it does with iterator blocks and captured variables. Checking reflector, I get:

[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
{
    // ... lots more here not shown
    public string <secret>5__1;
}
Scorify answered 22/10, 2012 at 11:39 Comment(3)
what about in cases of WCF? should I just use the OperationContext instead, provided that it gets migrated over to the new thread?Toscana
@Toscana if you mean the instance, then that should work. But I doubt that the static OperationContext.Current would work correctly. So var ctx = OperationContext.Current; at the top (on the original thread), and then refer only to ctx, not to OperationContext.CurrentScorify
so you're saying that unless you capture the current OperationContext in a closure as before await you won't get back the same instance of OperationContext after the await?Toscana
N
7

Getting a task continuation to execute on the same thread requires a synchronization provider. That's an expensive word, the simple diagnostic is by looking at the value of System.Threading.SynchronizationContext.Current in the debugger.

That value will be null in console mode app. There is no provider that can make code run on a specific thread in a console mode app. Only a Winforms or WPF app or ASP.NET app will have a provider. And only on their main thread.

The main thread of these apps do something very special, they have a dispatcher loop (aka message loop or message pump). Which implements the general solution to the producer-consumer problem. It is that dispatcher loop that allows handing a thread a bit of work to perform. Such a bit of work will be the task continuation after the await expression. And that bit will run on the dispatcher thread.

The WindowsFormsSynchronizationContext is the synchronization provider for a Winforms app. It uses Control.Begin/Invoke() to dispatch the request. For WPF it is the DispatcherSynchronizationContext class, it uses Dispatcher.Begin/Invoke() to dispatch the request. For ASP.NET it is the AspNetSynchronizationContext class, it uses invisible internal plumbing. They create an instance of their respective providers in their initialization and assign it to SynchronizationContext.Current

There's no such provider for a console mode app. Primarily because the main thread is entirely unsuitable, it doesn't use a dispatcher loop. You would have create your own, then also create your own SynchronizationContext derived class. Hard to do, you can't make a call like Console.ReadLine() anymore since that entirely freezes the main thread on a Windows call. Your console mode app stops being a console app, it will start resembling a Winforms app.

Do note that these runtime environments have synchronization providers for a good reason. They have to have one because a GUI is fundamentally thread-unsafe. Not a problem with the Console, it is thread-safe.

Natasha answered 22/10, 2012 at 12:6 Comment(1)
is there an equivalent synchronization provider in WCF?Toscana
B
0

Have a look on this thread

On fields marked with ThreadStaticAttribute, initialization occurs only once, in the static constructor. In your code when the new thread with ID 11 is created a new Secret field will be created but it is empty / null. When returning to the "Start" task after the await call the task will finish on thread 11 (as your printout shows) and therefore the string is empty.

You could solve your problem by storing the Secret in a local field inside "Start" just before calling Sleepy, then restore the Secret from the local field after returning from Sleepy. You could also do it in Sleepy just before you call "await Task.Delay(1000);" that actually causes the thread switch.

Brumby answered 10/2, 2017 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.