Skip to content
标签
欢迎扫码关注公众号

C# AsyncLocal

使用 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 或者 一个新的实例),则不会影响父线程的值,因为仅修改了当前线程中指向的地址,而并没有改变修改前地址指向的数据。

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.