Skip to content

C# AsyncLocal

🏷️ C#

使用 ThreadLocal 可以很方便的隔离多线程中的数据,但是在调用了异步方法时,就无法正确获取父线程中设置的 ThreadLocal 中的值。

在官方文档 AsyncLocal<T> Class 中的示例很清楚的说明了 ThreadLocalAsyncLocal 的区别。

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

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

    static ThreadLocal<string> _threadLocalString = new ThreadLocal<string>();

    static async Task AsyncMethodA()
    {
        // Start multiple async method calls, with different AsyncLocal values.
        // We also set ThreadLocal values, to demonstrate how the two mechanisms differ.
        _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 both calls
        await t1;
        await t2;
     }

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

    static async Task Main(string[] args)
    {
        await AsyncMethodA();
    }
}
// The example displays the following 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 ''

AsyncMethodB 方法中 await 后面的处理会在新的线程中执行,从而导致从 _threadLocalString 中无法获取到数据。
这是因为新的线程中还没有设置当前线程的该变量的值。

AsyncLocal 则是用来在异步处理处理中跨线程保存数据的。
根据上例的执行结果,await 后的处理中获取的 _asyncLocalString 的值是开启这个线程(示例中的 t1)时的值,之后再修改主线程中 _asyncLocalString 的值不会影响到这个线程(t1)中的值。

另外 AsyncLocal 还实现了一个可以指定值变更通知处理的构造函数,整个异步处理中的任一线程修改了值都会触发该处理。

2019/08/02 追记

使用 AsyncLocal 时会发生线程间数据的传递,那么在子线程或者父线程修改了数据是否会影响到另一个线程呢?
从上面官方的示例中其实已经展示了一点:在父线程中修改 AsyncLocal<string> 中的值时,子线程并没有随之更改。
为了确认是否会传递,将上面的代码修改如下:

csharp
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncLocalSample
{
    class Program
    {
        static AsyncLocal<string> _asyncLocalString = new AsyncLocal<string>();

        static ThreadLocal<string> _threadLocalString = new ThreadLocal<string>();

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

        static ThreadLocal<Dictionary<string, string>> _threadLocalDictionary = new ThreadLocal<Dictionary<string, string>>();

        const string KEY = "KEY";

        static async Task AsyncMethodA(string value)
        {
            Console.WriteLine("Exiting AsyncMethodA.");
            Console.WriteLine($@"   Expected '', 
        AsyncLocalString value is '{_asyncLocalString.Value}', 
        ThreadLocalString value is '{_threadLocalString.Value}', 
        AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}', 
        ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}', 
        ThreadId is {Thread.CurrentThread.ManagedThreadId}");

            // Start multiple async method calls, with different AsyncLocal values.
            // We also set ThreadLocal values, to demonstrate how the two mechanisms differ.
            _asyncLocalString.Value = value;
            _threadLocalString.Value = value;

            _asyncLocalDictionary.Value = new Dictionary<string, string>();
            _asyncLocalDictionary.Value.Add(KEY, value);
            _threadLocalDictionary.Value = new Dictionary<string, string>();
            _threadLocalDictionary.Value.Add(KEY, value);

            var t1 = AsyncMethodB(value);

            // Await both calls
            await t1;

            await Task.Delay(100);

            Console.WriteLine("Exiting AsyncMethodA.");
            Console.WriteLine($@"   Expected '{value}', 
        AsyncLocalString value is '{_asyncLocalString.Value}', 
        ThreadLocalString value is '{_threadLocalString.Value}', 
        AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}', 
        ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}', 
        ThreadId is {Thread.CurrentThread.ManagedThreadId}");
        }

        static async Task AsyncMethodB(string expectedValue)
        {
            Console.WriteLine("Entering AsyncMethodB.");
            Console.WriteLine($@"   Expected '{expectedValue}', 
        AsyncLocalString value is '{_asyncLocalString.Value}',
        ThreadLocalString value is '{_threadLocalString.Value}',
        AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
        ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
        ThreadId is { Thread.CurrentThread.ManagedThreadId}");

            await Task.Delay(100);

            _asyncLocalString.Value = $"{expectedValue} new";
            _threadLocalString.Value = $"{expectedValue} new";
            _asyncLocalDictionary.Value[KEY] = $"{expectedValue} new";
            if (_threadLocalDictionary.Value == null)
            {
                _threadLocalDictionary.Value = new Dictionary<string, string>();
                _threadLocalDictionary.Value.Add(KEY, $"{expectedValue} new");
            }
            else
            {
                _threadLocalDictionary.Value[KEY] = $"{expectedValue} new";
            }

            Console.WriteLine("Entering AsyncMethodB.");
            Console.WriteLine($@"   Expected '{expectedValue} new', 
        AsyncLocalString value is '{_asyncLocalString.Value}',
        ThreadLocalString value is '{_threadLocalString.Value}',
        AsyncLocalDictionary value is '{_asyncLocalDictionary.Value?[KEY]}',
        ThreadLocalDictionary value is '{_threadLocalDictionary.Value?[KEY]}',
        ThreadId is { Thread.CurrentThread.ManagedThreadId}");
        }

        static void Main(string[] args)
        {
            var t1 = AsyncMethodA("Value 1");
            t1.Wait();

            Thread.Sleep(1000);

            Console.WriteLine("----------------------------------------");

            var t2 = AsyncMethodA("Value 2");
            t2.Wait();

            Console.ReadLine();
        }
    }
}

运行结果:

AsyncLocal<Dictionary<string, string>> 中的值在子线程中修改,同时也改变了父线程中的值。但是 AsyncLocal<string> 就没有这个效果,子线程中的修改并没有传递到父线程。

这说明在创建子线程时AsyncLocal 会复制父线程中 AsyncLocal.Value 的地址到子线程的 AsyncLocal.Value,也就是仅拷贝了引用的地址

如果在子线程中修改了 AsyncLocal.Value 中的值则会同步的影响到父线程的值,因为指向的是堆中的同一个数据;如果重新指定了 AsyncLocal.Value 所指向的地址(比如设置为 null 或者 一个新的实例),则不会影响父线程的值,因为仅修改了当前线程中指向的地址,而并没有改变修改前地址指向的数据。