.NET 异步本地存储AsyncLocal

在文章".NET 线程本地存储(变量)ThreadStatic,ThreadLocal的使用"讲了两个多线程本地存储,但是有个问题,ThreadStatic和ThreadLocal如果异步跨线程的话会丢失值。怎么解决呢?AsyncLocal<T>正好弥补了这个问题。我们拿AsyncLocal<T>和ThreadLocal<string>做对比来实践操作,如下面案例:.​​​​​

 static AsyncLocal<string> _asyncstr = new AsyncLocal<string>(); static ThreadLocal<string> _threadstr = new ThreadLocal<string>(); private static async Task AsyncMethodA() {     _asyncstr.Value = "AsyncLocal 1";     _threadstr.Value = "ThreadLocal 1";     var t1 = AsyncMethodB("Value 1");     _asyncstr.Value = "AsyncLocal 2";     _threadstr.Value = "ThreadLocal 2";     var t2 = AsyncMethodB("Value 2");     await t1;//异步语法糖,底层会转换成状态机     await t2; } static async Task AsyncMethodB(string expectedValue) {     Console.WriteLine("AsyncMethodB开始");     Console.WriteLine("   传入值 '{0}', AsyncLocal的值是 '{1}', ThreadLocal的值是 '{2}' 开始线程Id:{3} ",                       expectedValue, _asyncstr.Value, _threadstr.Value, Thread.CurrentThread.ManagedThreadId);     await Task.Delay(100);//异步延迟 下面会开启一个新的线程,必须有await才会跨线程     Console.WriteLine("AsyncMethodB尾 ");     Console.WriteLine("   传入值 '{0}', AsyncLocal的值是'{1}', ThreadLocal的值是 '{2}' 尾线程Id:{3} ",                       expectedValue, _asyncstr.Value, _threadstr.Value, Thread.CurrentThread.ManagedThreadId); } static async Task Main(string[] args) {     await AsyncMethodA(); }

输出结果为:

.NET 异步本地存储AsyncLocal

从结果可以看出,当切换线程后,ThreadLocal<T>的值丢失了,而且切换线程后的线程Id也发生了变化,而AsyncLocal<T>的值未丢失。

AsyncLocal<T> 是什么东西呢?

    AsyncLocal<T> 对象接受一个泛型参数,其主要作用是保存异步等待上下文中共享某个变量的值。是在 .Net 4.6 之后推出的一个对象。

    在父子异步的情况下也适用AsyncLocal<T>,AsyncLocal 泛型类是值类型时父子任务的时候 ,任务赋值后父类不会修改,如果改成引用类型可以避免这个问题,如下代码:

     static AsyncLocal<string> _asyncstr = new AsyncLocal<string>();        private static async Task ParentMethod()        {            _asyncstr.Value = "父类赋值";            Console.WriteLine("父类1 AsyncLocal的值是 '{0}',  线程Id:{1} ",                   _asyncstr.Value, Thread.CurrentThread.ManagedThreadId);            await ChildMethod("Value 1");            Console.WriteLine("父类2 AsyncLocal的值是 '{0}',  线程Id:{1} ",            _asyncstr.Value, Thread.CurrentThread.ManagedThreadId);        }        static async Task ChildMethod(string expectedValue)        {            Console.WriteLine("子类1 AsyncLocal的值是 '{0}',  线程Id:{1} ",             _asyncstr.Value, Thread.CurrentThread.ManagedThreadId);            _asyncstr.Value = "子类赋值";            await Task.Delay(100);//异步延迟 下面会开启一个新的线程            Console.WriteLine("子类2 AsyncLocal的值是 '{0}',  线程Id:{1} ",            _asyncstr.Value, Thread.CurrentThread.ManagedThreadId);        }        static async Task Main(string[] args)        {            await ParentMethod();        }

结果为:

.NET 异步本地存储AsyncLocal

如果改进呢,可以把泛型类修改为引用类型,如下。

       public class Test        {            public string str { get; set; }        }        private static  readonly AsyncLocal<Test> _asyncstr = new AsyncLocal<Test>();        public static async Task ParentMethod()        {            _asyncstr.Value = new Test { str = "父类赋值" };             Console.WriteLine("父类1 AsyncLocal的值是 '{0}',  线程Id:{1} ",                   _asyncstr.Value.str, Thread.CurrentThread.ManagedThreadId);            await ChildMethod();            Console.WriteLine("父类2 AsyncLocal的值是 '{0}',  线程Id:{1} ",            _asyncstr.Value.str, Thread.CurrentThread.ManagedThreadId);        }       public static async Task ChildMethod()        {            Console.WriteLine("子类1 AsyncLocal的值是 '{0}',  线程Id:{1} ",             _asyncstr.Value.str, Thread.CurrentThread.ManagedThreadId);            _asyncstr.Value.str =  "子类赋值" ;//注意这里不能新实例化            await Task.Delay(100);//异步延迟 下面会开启一个新的线程            Console.WriteLine("子类2 AsyncLocal的值是 '{0}',  线程Id:{1} ",            _asyncstr.Value.str, Thread.CurrentThread.ManagedThreadId);        }        static async Task Main(string[] args)        {            await ParentMethod();        }

这样的结果

.NET 异步本地存储AsyncLocal

如果使用AsyncLocal<T>不想值跨线程呢?可以用ExecutionContext.SuppressFlow()方法禁止捕捉执行上下文,那么Task.Delay后的值将为null。代码如下:​​​​​​​

// 禁止捕捉执行上下文 ExecutionContext.SuppressFlow();await Task.Delay(100);//异步延迟 下面会开启一个新的线程​​​​​​​

用途:AsyncLocal<T>异步调用执行上下文值不发生变化,这在MVC5中请求的上下文的源码有用到,感兴趣的可以去看看源码。

后记

    ThreadLocal 是用于为不同的线程保存不同的变量值的,即同一个变量在不同线程当中存储的值可以不一样,部分解决了多线程脏读等问题。如果使用了 await 关键字会导致等待异步调用结束后代码已经被调度到其他的线程了。而 AsyncLocal<T> 正是为了处理这种情。写作水平有限,欢迎留言讨论。