What's the effect of AsyncLocal<T> in non async/await code?
Asked Answered
G

2

29

I'm working on a very large and old code base of a desktop winform application. In this code base there are lots of operations performed in background threads, mainly using BackgroundWorker.

A common pattern in this code base, is to hide complexity by binding artifacts to the thread being executed. For instance, the database connection and transaction are stored in [ThreadStatic] fields.

I'm trying to change this, and start using async/await code, and benefit from running the task in any thread of the pool, and allowing a task to continue executing in any other thread by using ConfigureAwait(false). I know that [ThreadStatic] doesn't play nice with async/await, and I've read several answers over here suggesting to use AsyncLocal<T> instead.

Given that I'm working on a large code base, as mentioned before, I'm unable to switch to async/await everywhere in a single shot, and I must do this changes gradually. So the code that before had [ThreadStatic] will change to AsyncLocal<T>, but large portions of the code will continue using BackgroundWorker and won't hit a single async/await line of code.

Question
Will this work? I need to be able to define some kind of context flow that will work with my new async/await code, and also keep working with my old non async code which relied on [ThreadStatic] keeping every thread stuff independent from each other.

If I'm totally wrong and going down the wrong path, suggestions are very welcomed.

Georgetta answered 22/3, 2017 at 17:43 Comment(0)
S
50

It should work.

AsyncLocal<T> is an abstraction of the logical call context. I describe the logical call context and how it interacts with async/await in detail in an old blog post.

In summary, it will probably work fine, but there is one aspect of AsyncLocal<T> that is quite different than ThreadStatic.

When you write to the AsyncLocal<T> value, that value is set for the current logical call context. An async method will establish a copy-on-write scope for its logical call context, so if you write to it within an async method, it will create a new logical call context that contains the new value. This allows async method to use it in a nested fashion, where "inner" contexts can overwrite "outer" contexts. However, the "inner" context values never flow back to the caller; when the "outer" context is resumed, it completely replaces the "inner" context.

If none of the methods are async and the values are only set from their own threads, then that thread just has a single logical call context, and writing/reading the values will work just the same as ThreadStatic.

Submicroscopic answered 22/3, 2017 at 18:22 Comment(0)
F
2

Stephen provided a very detailed explanation for AsyncLocal<T> but I will try to simplify it and also show how to make mentioned behavior of AsyncLocal<T> nested contexts the same as ThreadStatic if necessary.

So simply speaking the value of type T in AsyncLocal<T> is copied by value to every inner logical call context. This is similar to passing some simple value parameter (for example int) to nested sub-functions. Changing its value in any sub-function will never affect the value in the caller function.

But in some cases for example, when creating fakes for asynchronous tests you need to change this behavior of AsyncLocal<T> (and have it the same as ThreadStatic). As a workaround, you can use a wrapper class holding the target value:

public class SimpleWrapper<T>
{
    public T? Value { set; get; }

    public SimpleWrapper()
    {
    }

    public SimpleWrapper(T value)
    {
        Value = value;
    }
}

and then use it instead of T like this:

private static readonly AsyncLocal<SimpleWrapper<int>> Counter = new();

In this example, you can change the Counter.Value.Value at any time and this will affect all logical call contexts in the current async flow.

Fornication answered 13/2, 2023 at 18:19 Comment(2)
Thanks for the insight. Did you mean "you can change the Counter.Value.Value at any time and this will affect all logical call contexts" by any chance?Georgetta
Of course Counter.Value.Value. Thanks! FixedFornication

© 2022 - 2024 — McMap. All rights reserved.