【橙子老哥】cAsyncLocal底层原理

程序员有二十年 2024-09-30 12:04:45

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 }; } }```
0 阅读:0
程序员有二十年

程序员有二十年

感谢大家的关注