hello,大家好,又到了橙子老哥的分享时间,希望大家一起学习,一起进步。
欢迎加入.net意社区,第一时间了解我们的动态,地址:ccnetcore.com
废话少说,我们直接开始
1、ThreadLocal与AsyncLocal
众所皆知,AsyncLocal是用于异步方法之间的数据隔离,而 ThreadLocal是用于多线程之间的数据隔离,需要明白,多线程 != 异步,多线程只是异步的一种实现,两者完全不是同一水平的东西,不能进行比较
关于他们的区别,相信大家看过很多的文章了,我总结放两个例子,不多赘述,带过即可
```csharpAsyncLocal<Student> context =new AsyncLocal<Student>();await Task.Run(async () =>{ context.Value = new Student { Name = $"张三" }; Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(1000); Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); return Task.CompletedTask;});``````csharp输出结果:(正确)值:张三,ThreadId=9值:张三,ThreadId=7```如果改为:
```csharpThreadLocal<Student> context =new ThreadLocal<Student>();await Task.Run(async () =>{ context.Value = new Student { Name = $"张三" }; Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(1000); Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); return Task.CompletedTask;});``````csharp输出结果:(错误)值:张三,ThreadId=6 (随缘)值:,ThreadId=8```2、探究AsyncLocal原理
又到了大家最爱的探究环境,让我们深入看看AsyncLocal的源码
最外层的源码不多:
```csharp [Maybe] public T Value { get { object? value = ExecutionContext.GetLocalValue(this); if (typeof(T).IsValueType && value is ) { return default; } return (T)value!; } set { ExecutionContext.SetLocalValue(this, value, _valueChangedHandler is not ); } }```主要是一个get和set,对应的就是value方法的赋值和查询
按照以往惯例,get方法,ExecutionContext.GetLocalValue(this)肯定很简单,不出意外:
```csharp internal static object? GetLocalValue(IAsyncLocal local) { ExecutionContext? current = Thread.CurrentThread._executionContext; if (current == ) { return ; } Debug.Assert(!current.IsDefault); Debug.Assert(current.m_localValues != , "Only the default context should have , and we shouldn't be here on the default context"); current.m_localValues.TryGetValue(local, out object? value); return value; }```只是从`current.m_localValues`中根据IAsyncLocal的引用,获取到值而已
那么我们想要追踪到源头,就要看`current.m_localValues`的值怎么给进去的了,我们看看set
```csharp //设置值的方法 internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications) { ExecutionContext? current = Thread.CurrentThread._executionContext; //判断设置的心值和旧值是否相同 object? previousValue = ; bool hadPreviousValue = false; if (current != ) { hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue); } //相同的话不在进行设置直接返回 if (previousValue == newValue) { return; } if (current != ) { //设置新值 newValues = current.m_localValues.Set(local, newValue, treatValueAsNonexistent: !needChangeNotifications); } else { //如果没有使用过先初始化在存储 newValues = AsyncLocalValueMap.Create(local, newValue, treatValueAsNonexistent: !needChangeNotifications); } //给当前线程执行上下文赋值新值 Thread.CurrentThread._executionContext = (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ? : new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed); }```我们来看它传递的值`newValues`,最终也是走进了`IAsyncLocalValueMap? m_localValues`
看到这里应该能明白,这里赋值到`ExecutionContext`上下文的`IAsyncLocalValueMap? m_localValues`中,然后get的时候再查出来
AsyncLocal只是对ExecutionContext进行一层包装,而真正数据流转,统一交给了ExecutionContext操作,至于`ExecutionContext`的操作,篇幅较多,后续会单独出一期进行深入刨析,大致流程如下:
当我们切换线程的时候,就会将上下文进行传递出去,最终交给操作系统,当我们切换回来的时候,又会执行回调,将原先的copy的数据进行恢复
3、常见问题
我们先抛砖引玉,有这么一种情况,当我们的泛型是一个对象,并在子异步方法里面进行赋值,并不会影响到外层的数据
```csharpAsyncLocal<Student> context =new AsyncLocal<Student>();context.Value = new Student {Name = "张三" };Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");await Task.Run(() =>{ Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("设置李四"); context.Value = new Student {Name = "李四" }; Console.WriteLine($"Task1之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");});Console.WriteLine($"Main之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");``````csharp输出结果:Main之前:张三,ThreadId=1Task1之前:张三,ThreadId=6设置李四Task1之后:李四,ThreadId=6Main之后:张三,ThreadId=6```如果我们按照面向过程的思维去考虑,最后一个Main之后输出的竟然不是已经赋值的李四
类似出现了被回档的情况,这个也是非常多刚接触AsyncLocal 容易犯下的错误,那我们来说说,为什么会出现这个问题
我们看看这个:
赋值的时候,传递的key是一个引用地址
我们想想分析一下,有两个原因:
查找值的key是一个引用地址
线程切换传递的是一个浅拷贝对象
如果,我们不更改它的引用,不就可以实现传递了吗?
更改后代码:
```csharpAsyncLocal<Student> context =new AsyncLocal<Student>();context.Value = new Student {Name = "张三" };Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");await Task.Run(() =>{ Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("设置李四"); var data= context.Value; data.Name="李四"; Console.WriteLine($"Task1之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");});Console.WriteLine($"Main之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");返回结果:Main之前:张三,ThreadId=1Task1之前:张三,ThreadId=6设置李四Task1之后:李四,ThreadId=6Main之后:李四,ThreadId=8```对头,这就是解决方案
4、扩展
如果有看过`HttpContextAccessor`的源码,肯定对这个很熟悉
因为它也是这么玩的
```csharp private static readonly AsyncLocal<HttpContextAccessor.HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextAccessor.HttpContextHolder>(); public HttpContext? HttpContext { get => HttpContextAccessor._httpContextCurrent.Value?.Context; set { HttpContextAccessor.HttpContextHolder httpContextHolder = HttpContextAccessor._httpContextCurrent.Value; if (httpContextHolder != ) httpContextHolder.Context = (HttpContext) ; if (value == ) return; HttpContextAccessor._httpContextCurrent.Value = new HttpContextAccessor.HttpContextHolder() { Context = value }; } }```