Avoid ThreadStatic, ThreadLocal and AsyncLocal. Float the state instead!

In the article on The dangers of ThreadLocal I explained how the introduction of async/await forces us to unlearn what we perceived to be true in the “old world” when Threads dominated our software lingo. We’re now at a point where we need to re-evaluate how we approach thread safety in our codebases while using the async/await constructs. In today’s post-thread era, we should strive to remove all thread (task-local) state and let the state float into the code which needs it. Let’s take a look at how we can implement the idea of floating state and make our code robust and ready for concurrency.

Identifying the context

Consider the following example where we have a WorkerOrchestrator which has the responsibility to orchestrate multiple asynchronous workers.

    class WorkerOrchestrator  {
      public Task DoWork(int concurrency) {
          var worker = new Worker();
          var workTasks = new Task[concurrency];
          for (int i = 0; i < concurrency; i++) {
              workTasks[i] = worker.Work();
          }
          return Task.WhenAll(workTasks);
      }
    }

The orchestrator creates a Worker instance and calls the Work method multiple times based on the provided concurrencynumber. Instead of awaiting each worker’s task to be finished, it adds each of the tasks to an array. Note that the code does not await the worker.Work() invocation as that would cause the work to be done by the workers one-by-one instead of running them concurrently. At the end of the DoWork method the worker tasks are passed into Task.WhenAll, which will only complete when all of the tasks in the collection have been completed. Let’s have a look at the Worker implementation.

    class Worker {
        static ThreadLocal<Validator> validator = new ThreadLocal<Validator>(() => new Validator());

        public async Task Work() {
           await Step1();
           await Step2();
        }

        Task Step1() {
           return validator.Value.Validate(new Step1Context());
        }

        Task Step2() {
           return validator.Value.Validate(new Step2Context());
        }
    }

When data inside classes, such as fields or properties, is allocated it exists within the context of the instance of the class that owns it. Sometimes it is desirable to store context-related data that automatically follows the flow of execution between execution scopes. Such data is no longer owned by an instance of a class. Instead it is owned by the execution flow.

The pattern that implements execution-scoped data is called Ambient Context. We can find many examples of Ambient Context in the .NET Framework. When you are using TransactionScope, the current transaction that it sets can be found inTransaction.Current. In web applications HttpContext.Current represents the data of the currently handled HTTP call.

In the example above, the Worker class has a ThreadLocal variable internally which creates a new instance of Validator for each Thread accessing it. The Worker implementation uses the current threading ambient context as a ThreadLocal. Each call to the worker’s Work method issued by a thread will create a new instance of the Validator. All calls happening on the same thread can access the same instance of a Validator.

Code calling the worker’s Work method can’t possibly know which context the Validator instance exists in since the validator isn’t part of the public contract on the Worker class. Because of this, it is not possible to have the Validatorinstance run with the same context as the calling code. This will result in the possibility of multiple instances of a Validatorexiting inside the call stack of a Work method. Remember, each await statement is an opportunity for the calling thread to work on other tasks and there is no guarantee that the same thread will execute the code after the await statement.

Knowing that this may happen, we need to figure out how we can get rid of the ambient context without breaking the thread-safety of our code.

Floating the state

The best possible way is to declare a dependency to a Validator instance at the method level (also called method-injection). Let’s see how this change would look when applied to the Worker class:

    class Worker {

        public async Task Work(Validator validator) {
            await Step1(validator);
            await Step2(validator);
        }

        Task Step1(Validator validator) {
            return validator.Validate(new Step1Context());
        }

        Task Step2(Validator validator) {
            return validator.Validate(new Step2Context());
        }
    }

As you can see, the methods were modified to accept a Validator instance. This design change “ripples” from the internal methods, Step1 and Step2, up to the Work method on the public interface of the worker. Let’s see how this impacts theWorkerOrchestrator:

    public class WorkerOrchestrator {
           public Task DoWork(int concurrency) {
              ...
               for (i = 0; i < concurrency; i++) {
                   workTasks[i] = worker.Work(new Validator());
               }
              ...
           }
    }

The implication of this change is that the caller of the worker is now required to pass in an instance of a Validator every time it calls the Work method.

Drifting away

Instead of creating a hidden state in the Worker class which has to be stored with almost magical constructs like AsyncLocal, we changed our design to expose a method-level dependency on the worker’s Work method. By doing this, the design of the worker and the requirements for the caller become more intention revealing. As small as this change might seem, by applying this principle to your codebases you will achieve a more functional-friendly design. Additionally, you will inherit one of the biggest advantages of functional design: thread-safety.

About the author

Daniel Marbach

8 comments

  • When it doesn’t significantly affect the usability of an API then this is fine, but what you’re effectively saying is that transaction scope should have been implemented by having to pass an instance of it into every single call that needs it.

    You didn’t really explain why AsyncLocal is bad other than saying not to use it in the title. What exactly do you have against it? Everything we do now as developers is “almost magical”, like say the async keyword. How is AsyncLocal any more magical than async/await?

  • I’m not saying it should. My point was explicitly floating the dependencies into the code that needs it over method parameter passing is more intention revealing. Look for example at how cancellation is implemented. It is a cooperative cancelation model where the code that supports cancellation needs to indicate that by accepting a cancellation token as a parameter. Then it is the responsibility of the caller to decide on what level the cancellation scope begins and float it into the methods. The same could be argued with transactions. Explicitly passing a Committable Transaction object would clearly show that the code in question can participate in transactions. I’m aware that the world of software development is not black and white. Sometimes ambient state is OK or even the only solution. I’m challenging here to not always fall back to that approach just because it looks convenient.

    Ambient state is magic because it is not visible and the state is bound to a lifetime external to the instance that declares the state. It is basically owned by some other notion. Based on that it is difficult to understand how the execution context flows and what state will be present at what time.

    async/await keyword is simpler because if you leave concurrency out of the game the code execution flow is not different from synchronous execution. The only difference is that we give the runtime opportunity to free up the calling thread (I deliberately left out the context capturing thing because, YES, THAT’S A MESS ๐Ÿ˜‰ )

  • This is very instructive about how ThreadLocal works, but what is the purpose of making it thread local if you just pass in a reference to methods to be used by other threads? Interesting way to just undo the ThreadLocalness temporarily.

  • Hi Daniel,

    Thanks for an interesting article! I like your “float the state” expression ๐Ÿ™‚ It’s really catchy and makes the approach stick.

    You’ve explained very well the pros of having the state float explicitly instead of being passed “magically” as a part of some invisible context. Still, there are valid cases for having an ambient context.

    Jon Skeet wrote an excellent short blog post discussing the pros and cons of both approaches: https://codeblog.jonskeet.uk/2010/11/08/the-importance-of-context-and-a-question-of-explicitness/

    In the post, he presents the same arguments that you’ve given, but also their counterparts. I usually recommend that post as a starting point for everyone who wants to get a balanced view of the topic.

    Speaking of real-life examples, in my recent consulting activity a customer needed to pass the logged user to several parts of the system. The initial implementation they had used the “flow the state” approach but turned out to be very cumbersome over time because of several reasons (all of them listed in Jon’s post). After we switched to the ambient context approach things actually got much simpler ๐Ÿ˜‰ To quote Jon Skeet: “Ultimately it feels like a battle between purity and pragmatism: being explicit helps to keep your code purer, but it can mean a lot of fluff around your real logic, just to maintain the required information to pass onward.”

    Thanks once again for a great article!

  • I had hoped you would explain how cool and useful AsyncLocal is and how to use it correctly but instead you’re writing we should avoid it. i’m disappointed by your article and find it neither useful nor helpful.

  • There are several cases in enterprise software when method injection is not an option. There are situations when ambient contexts are useful and necessary. In an enterprise applications, code re-usability and component re-usability is more important than being afraid of using advanced patterns such as ambient context. With components being called by different subsystems or even running on different devices, a component or object graph should not know anything about their calling context. In these cases the method injection pattern would have required these components to depend on code that they should not be aware of. In cases of internal frameworks commonly used behaviors are hidden in base classes that should not depend on code that supposed to implement or use them. In these cases there are not always a chance to pass around context like objects through the whole layered architecture only for the sake of injecting them into some base class method because it should know something contextual.

    I was also hoping your article would explain how to use AsyncLocal in enterprise scenarios along with DI frameworks, background operations, TPL for example and advanced programming situations. But writing such an article just to tell everyone that you are not capable of using an advanced construct so everyone else should also be afraid of it. I am really disappointed.

  • Hi Daniel,
    You are absolutely right that sometimes circumstances require to go one way or another. The intention of this series was to show another path instead of relying on ambient context because that’s something in my opinion default towards slightly too much.

    Regarding code re-usability and component re-usability as well as frameworks with base classes I’m not going to argue with you because I think everyone has their preferred way of designing something based on their experiences with one way or the other. In case you are interested and want to chat I’m more than happy to do a quick Zoom call or something like that to present my viewpoint if that helps.

    Last but not least about your expectation about this post. I’m sorry that they haven’t been met. I find it interesting though that you jump to conclusions about my capabilities

    > you are not capable of using an advanced construct

    Anyway thanks for leaving your criticism here.

By Daniel Marbach

Recent Posts