在文章".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();}
输出结果为:

从结果可以看出,当切换线程后,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();}
结果为:

如果改进呢,可以把泛型类修改为引用类型,如下。
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();}
这样的结果

如果使用AsyncLocal<T>不想值跨线程呢?可以用ExecutionContext.SuppressFlow()方法禁止捕捉执行上下文,那么Task.Delay后的值将为null。代码如下:
// 禁止捕捉执行上下文ExecutionContext.SuppressFlow();await Task.Delay(100);//异步延迟 下面会开启一个新的线程
用途:AsyncLocal<T>异步调用执行上下文值不发生变化,这在MVC5中请求的上下文的源码有用到,感兴趣的可以去看看源码。
后记
ThreadLocal 是用于为不同的线程保存不同的变量值的,即同一个变量在不同线程当中存储的值可以不一样,部分解决了多线程脏读等问题。如果使用了 await 关键字会导致等待异步调用结束后代码已经被调度到其他的线程了。而 AsyncLocal<T> 正是为了处理这种情。写作水平有限,欢迎留言讨论。