在文章".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> 正是为了处理这种情。写作水平有限,欢迎留言讨论。