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