AsyncLocal vs ThreadLocal
AsyncLocal vs ThreadLocal

AsyncLocal vs ThreadLocal

??Introduction

Let’s be honest - working with async programming and multithreading in .NET can be a headache. If you’ve ever tried to manage context-specific data, you’ve probably come across AsyncLocal and ThreadLocal. They sound kinda similar, but they behave completely differently. Pick the wrong one, and you might end up debugging some weird, mind-bending issues.

So, let’s break it down. No fluff, just clear explanations and a real example to make sense of it all.


??What are AsyncLocal and ThreadLocal?

AsyncLocal

AsyncLocal<T> lets you store data tied to the logical execution context of an asynchronous operation. In simple terms, no matter which thread your async code is running on, the value stays consistent throughout the asynchronous flow. It’s meant for things like correlation IDs, scoped states, or anything you want to propagate through your async/await calls.

??The value follows the async flow; it doesn’t care if a thread switch happens.

??Any changes to the value in one async operation will not affect unrelated

ThreadLocal

ThreadLocal<T> is completely different. It ties your data to the physical thread you're running on. Each thread has its separate copy of the value, and if your code switches threads (which is super common with async), you lose access to any value you set earlier.

??It’s all about the physical thread, not the logical flow.

??If your code switches threads (like during an await), your ThreadLocal values don’t carry over.

??It works great for thread-specific data in multi-threaded code (e.g., data caches or simulations).


??AsyncLocal and ThreadLocal in Action

Let’s look at an example from the Microsoft documentation to understand better how these two classes behave in real code. Take a moment to review it, and we’ll break it down step by step.

using System;
using System.Threading;
using System.Threading.Tasks;

class Example
{
    static AsyncLocal<string> _asyncLocalString = new();
    static ThreadLocal<string> _threadLocalString = new();

    static async Task AsyncMethodA()
    {
        _asyncLocalString.Value = "Value 1";
        _threadLocalString.Value = "Value 1";
        var t1 = AsyncMethodB("Value 1");

        _asyncLocalString.Value = "Value 2";
        _threadLocalString.Value = "Value 2";
        var t2 = AsyncMethodB("Value 2");

        await t1;
        await t2;
    }

    static async Task AsyncMethodB(string expectedValue)
    {
        Console.WriteLine($"Entering AsyncMethodB.");
        Console.WriteLine($"   Expected '{expectedValue}', AsyncLocal value is '{_asyncLocalString.Value}', ThreadLocal value is '{_threadLocalString.Value}'");
        await Task.Delay(100);
        Console.WriteLine($"Exiting AsyncMethodB.");
        Console.WriteLine($"   Expected '{expectedValue}', got '{_asyncLocalString.Value}', ThreadLocal value is '{_threadLocalString.Value}'");
    }

    static async Task Main()
    {
        await AsyncMethodA();
    }
}        

When you run this code, here’s the output:

Entering AsyncMethodB.
Expected 'Value 1', AsyncLocal value is 'Value 1', ThreadLocal value is 'Value 1'
---
Entering AsyncMethodB.
Expected 'Value 2', AsyncLocal value is 'Value 2', ThreadLocal value is 'Value 2'
---
Exiting AsyncMethodB.
Expected 'Value 2', got 'Value 2', ThreadLocal value is ''
---
Exiting AsyncMethodB.
Expected 'Value 1', got 'Value 1', ThreadLocal value is ''        

Let’s break down exactly what’s happening, step by step:

1?? Setting Initial Values

In AsyncMethodA, we:

??Set _asyncLocalString and _threadLocalString to "Value 1".

??Call AsyncMethodB("Value 1"), creating Task 1.

??Before awaiting Task 1, we change both variables to "Value 2".

??Call AsyncMethodB("Value 2"), creating Task 2.

At this point:

??Task 1 starts with _asyncLocalString = "Value 1".

??Task 2 starts with _asyncLocalString = "Value 2".

Here’s the key difference:

??ThreadLocal: Both tasks start on the same thread, so _threadLocalString holds "Value 2" for both tasks.

??AsyncLocal: Each task starts with the logical execution context in which it was created. That’s why Task 1 still sees "Value 1", while Task 2 sees "Value 2".


2?? Entering AsyncMethodB

Both tasks print their values:

??Task 1: AsyncLocal = "Value 1", ThreadLocal = "Value 1".

??Task 2: AsyncLocal = "Value 2", ThreadLocal = "Value 2".

Everything looks good so far - both AsyncLocal and ThreadLocal match the values set before the task started.


3?? await Task.Delay(100) - The Big Switch ??

Here’s where things get interesting. Calling await Task.Delay(100) causes both tasks to pause and likely resume on different threads. At this point:

??ThreadLocal is tied to the original thread, but since tasks now run on different threads, their ThreadLocal values are lost.

??AsyncLocal remains intact because it follows the logical execution context, not the physical thread.


4?? Exiting AsyncMethodB

When each task resumes:

??Task 1: AsyncLocal = "Value 1", ThreadLocal = "" (empty, because it resumed on a different thread).

??Task 2: AsyncLocal = "Value 2", ThreadLocal = "".

This explains why ThreadLocal values disappear after await, while AsyncLocal continues to hold the correct values for each task.


??AsyncLocal: When It Goes Wrong

Although AsyncLocal is incredibly powerful, it can be dangerous if misused. Let’s cover some common pitfalls to avoid.

1. Storing Disposable Objects

? BAD: Storing disposable objects in AsyncLocal is a bad idea because the execution context behaves as copy-on-write. Even if one logical flow disposes of an object, tasks working with a previously copied execution context can still hold references to the disposed object, leading to invalid states.

static AsyncLocal<MyDbContext> _context = new AsyncLocal<MyDbContext>();        

? GOOD: Explicitly pass disposable objects as method parameters or manage them with dependency injection.

2. Non-Thread-Safe Objects

? BAD: AsyncLocal's values can be accessed across multiple threads. If you store non-thread-safe objects (like a regular Dictionary), you risk race conditions and a corrupted state.

static AsyncLocal<Dictionary<int, string>> _dict = new AsyncLocal<Dictionary<int, string>>();        

? GOOD: Use thread-safe collections, like ConcurrentDictionary, if you need to store complex data in AsyncLocal.

3. Execution Context Leaks

? BAD: Some APIs (like Task.Run or CancellationToken.Register) capture the entire execution context, including AsyncLocal data. If you’re not careful, this can lead to memory leaks where unexpected data continues to live longer than it should.

cancellationToken.Register(() => SomeMethod());        

? GOOD: Use APIs like UnsafeRegister on CancellationToken to avoid execution context capturing.

cancellationToken.UnsafeRegister(() => SomeMethod());        

??When to Use What

Use AsyncLocal when you're working with async/await flows and need to propagate data consistently across method calls—even when thread switches occur. It ensures that context-specific data remains intact throughout the logical execution path, making it a great choice for:

??Correlation IDs – Maintaining traceability in distributed systems, logging, or telemetry.

??User Context – Carrying session or request-specific user data through an async call stack.

??Scoped States – Propagating values like locale settings or feature flags within an async operation.

??Dependency Injection Scenarios – Storing request-scoped dependencies in web applications.

?? Be careful when using AsyncLocal! Since it follows the logical execution flow, incorrect usage can lead to memory leaks, unexpected behavior in long-running tasks, and issues when combined with execution context capturing.


ThreadLocal

Use ThreadLocal in multi-threaded (non-async) scenarios, where each physical thread requires its own isolated data that should not be shared across threads. Since ThreadLocal is tied to a specific OS thread, it works well in cases such as:

??Thread-Specific Caching - Holding temporary data needed by computations running on individual threads.

??Parallel Algorithms - Storing intermediate results that should remain separate for each thread.

??Thread Pools - Keeping unique thread-local data in long-running worker threads. ??Simulations & Computational Models - Where each thread needs to track its own state without interference from others.

?? Be careful when using ThreadLocal! Since ThreadLocal values do not persist across await calls, they will be lost when an async method resumes execution on a different thread. Also, failing to dispose of ThreadLocal variables can lead to memory bloat, as values are not automatically garbage-collected until the thread itself terminates.


?TL;DR

??AsyncLocal follows async flow and survives thread switches.

??ThreadLocal is thread-bound and doesn’t persist across await.

??Use AsyncLocal for request-scoped or context-related data.

??Use ThreadLocal for thread-specific data that doesn’t need to survive an await.

要查看或添加评论,请登录

Serhii Kokhan的更多文章

  • Enhancing .NET Logging with Serilog Custom Enrichers

    Enhancing .NET Logging with Serilog Custom Enrichers

    What Are Enrichers in Serilog? Enrichers in Serilog are components that automatically add extra context to log events…

    2 条评论
  • Data Synchronization in Chrome Extensions

    Data Synchronization in Chrome Extensions

    Introduction Data synchronization in Chrome extensions is a common challenge, especially for various tools ranging from…

  • Dalux Build API Changelog

    Dalux Build API Changelog

    Dalux unfortunately does not provide an official changelog for their API updates. To help developers stay informed, I…

    2 条评论
  • JSONB in PostgreSQL with EF Core - Part 2

    JSONB in PostgreSQL with EF Core - Part 2

    Introduction Welcome back to the second part of our series on using JSONB in PostgreSQL with EF Core. In our previous…

  • Proxy vs Reverse Proxy in the .NET 8 Universe

    Proxy vs Reverse Proxy in the .NET 8 Universe

    Today, we're diving into the world of proxies – but not just any proxies. We're talking about the classic proxy and its…

  • JSONB in PostgreSQL with EF Core

    JSONB in PostgreSQL with EF Core

    Introduction JSONB in PostgreSQL is a big step forward for database management. It mixes the best parts of NoSQL and…

    8 条评论
  • Mastering the use of System.Text.Json

    Mastering the use of System.Text.Json

    Introduction Handling JSON data is a daily task for many developers, given its widespread use in modern applications…

  • Firebase Multitenancy & .NET 7

    Firebase Multitenancy & .NET 7

    Introduction Firebase is a leading platform for developing mobile and web applications, offering a variety of tools and…

  • How to use Azure Maps in Blazor

    How to use Azure Maps in Blazor

    Introduction Blazor, a powerful and versatile framework for building web applications, allows developers to utilize C#…

  • Azure SQL Database Scaling

    Azure SQL Database Scaling

    Introduction In today's fast-paced digital world, enterprises must be agile and scalable to remain competitive. For…