ThreadLocal相信不少童鞋用过,但AsyncLocal具体使用包括我在内的一大部分童鞋应该彻底没怎么使用过。
缓存
AsyncLocal一样出如今.NET Framework 4.6+(包括4.6),固然在.NET Core中没有版本限制即CoreCLR,对此类官方所给的解释是:将本地环境数据传递到异步控制流,例如异步方法
安全
又例如缓存WCF通讯通道,可使用AsyncLocal而不是.NET Framework或CoreCLR所提供的ThreadLocalapp
官方概念解释在咱们初次听来好像仍是有点抽象,不打紧,接下来咱们经过实际例子来进行详细说明和解释
框架
AsyncLocal和ThreadLocal区别异步
首先咱们先看以下例子,而后再分析两者和什么有关系async
private static readonly ThreadLocal<string> threadLocal = new ThreadLocal<string>();
private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
threadLocal.Value = "threadLocal";
asyncLocal.Value = "asyncLocal";
await Task.Yield();
Console.WriteLine("After await: " + threadLocal.Value);
Console.WriteLine("After await: " + asyncLocal.Value);
Task.Run(() => Console.WriteLine("Inside child task: " + threadLocal.Value)).Wait();
Task.Run(() => Console.WriteLine("Inside child task: " + asyncLocal.Value)).Wait();
Console.ReadLine();
}
猜猜如上将会打印出什么结果呢?ide
为什么ThreadLocal所打印的值为空值呢?咱们不是设置了值吗?此时咱们将要从执行环境开始提及
源码分析
若彻底理解ExecutionContext与SynchronizationContext两者概念和关系,理论上来说则可解答出上述问题,这里咱们简单叙述下,更详细介绍请查阅相关资料自行了解ui
ExecutionContext俗称“执行上下文”,也就是说和“环境”信息相关,这也就意味着它存储着和咱们当前程序所执行的环境相关的数据,这类环境信息数据存储在ThreadStatic或ThreadLocal中,换句话说ThreadLocal和特定线程相关spa
上述咱们讨论的是相同环境或上下文中,如果不一样上下文即不一样线程中,那状况又该如何呢?
在异步操做中,在某一个线程中启动操做,但却在另外一线程中完成,此时咱们将不能利用ThreadLocal来存储数据,因线程切换所需存储数据,咱们能够称之为环境“流动”
对于逻辑控制流,咱们指望的是执行环境相关数据能同控制流一块儿流动,以便能让执行环境相关数据能从一个线程移动到另一个线程,ExecutionContext的做用就在于此。而SynchronizationContext是一种抽象,好比Windows窗体则提供了WindowsFormSynchronizationContext上下文等等
SynchronizationContext做为ExecutionContext执行环境的一部分
ExecutionContext是当前执行环境,而SynchronizationContext则是针对不一样框架或UI的抽象
咱们可经过SynchronizationContext.Current获得当前执行环境信息。
到这里想必咱们已经明白基于特定线程的ThreadLocal在当前线程设置值后,但await却不在当前线程,因此打印值为空,若将上述第一个await去除,则可打印出设置值,而AsyncLocal倒是和执行环境相关,也就是说与线程和调用堆栈有关,并不针对特定线程,它是流动的。
AsyncLocal原理初步分析
首先咱们经过一个简单的例子来演示AsyncLocal类中值变化过程,咱们能从表面上可得出的结论,而后最终结合源码进行进一步分析
private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
asyncLocal.Value = "asyncLocal";
Task.Run(() =>
{
asyncLocal.Value = "inside child task asyncLocal";
Console.WriteLine($"Inside child task: {asyncLocal.Value}");
}).Wait();
Console.WriteLine($"after await:{asyncLocal.Value}");
Console.ReadLine();
}
由上打印咱们可看出,在Task方法内部将其值进行了修改并打印出修改事后的结果,在Task结束后,最终打印的倒是初始值。
在Task方法内部修改其值,但在任务结束后仍为初始值,这是一种“写时复制”行为,AsyncLocal内部作了两步操做
进行AsyncLocal实例的拷贝副本,但这是浅复制行为而非深复制
在设置新的值以前完成复制操做
接下来咱们再经过一个层层调用例子并深刻分析
private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
Demo1().GetAwaiter().GetResult();
Console.ReadLine();
}
static async Task Demo1()
{
await Demo2();
Console.WriteLine($"inside the method of demo1:{asyncLocal.Value}");
}
static async Task Demo2()
{
SetValue();
Console.WriteLine($"inside the method of demo2:{asyncLocal.Value}");
}
static void SetValue()
{
asyncLocal.Value = "initial value";
}
咱们看到此时在Demo1方法内部打印值为空,由于在Demo2方法内部并未使用异步,因此能打印出所设置的值,这说明以下问题
每次进行实际的aysnc/await后,都会启动一个新的异步上下文,而且该上下文与父异步上下文彻底隔离且独立,换句话说,在异步方法内,可查询本身所属AsyncLocal<T>,以便能确保不会污染父异步上下文,由于所作更改彻底是针对当前异步上下文的本地内容
至于为什么在Demo1方法内部打印为空,想必咱们已经很清晰,当async方法返回时,返回的是父异步上下文,此时将看不到任何子异步上下文所执行的修改。
AsyncLocal原理源码分析
咱们来到AsyncLocal类,经过属性Value设置值,内部经过调用ExecutionContext类中的SetLocalValue方法进行设置,源码以下:
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;
}
IAsyncLocal[]? newChangeNotifications = null;
IAsyncLocalValueMap newValues;
bool isFlowSuppressed = false;
if (current != null)
{
isFlowSuppressed = current.m_isFlowSuppressed;
newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
newChangeNotifications = current.m_localChangeNotifications;
}
else
{
newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
}
if (needChangeNotifications)
{
if (hadPreviousValue)
{
Debug.Assert(newChangeNotifications != null);
Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
}
else if (newChangeNotifications == null)
{
newChangeNotifications = new IAsyncLocal[1] { local };
}
else
{
int newNotificationIndex = newChangeNotifications.Length;
Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
newChangeNotifications[newNotificationIndex] = local;
}
}
Thread.CurrentThread._executionContext =
(!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
null :
new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);
if (needChangeNotifications)
{
local.OnValueChanged(previousValue, newValue, contextChanged: false);
}
}
当首次设置值时,咱们经过Thread.CurrentThread.ExecutionContext,获取其属性将为空,经过AsyncLocalValueMap.Create建立一个AsyncLocal实例并设置值
同时咱们也能够看到,若在同一执行环境中,当前最新设置值与以前所设置值相同,此时将不会是覆盖,而是直接返回。
咱们直接来到最后以下几行代码:
Thread.CurrentThread._executionContext =
(!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
null :
new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);
若默认使用Task默认线程池调度,即便线程池重用线程,其执行环境上下文也会不一样,如此可说明将更能保证不会将线程数据泄露到另一个线程中,也就是说在重用线程时,但将会保证异步本地实例会按照预期进行GC(我的觉得,理论上状况应该是这样,这样也能保证AsyncLocal是安全的)。
至于其余关于如何进行值更改后事件通知,这里就再也不额外展开叙述
因为AsyncLocal使用浅拷贝,咱们应保证存储的数据类型不可变,若要修改AsyncLocal<T>实例值,必须保证异步上下文隔离且相互不会影响。
到这里咱们已彻底清楚,AsyncLocal是针对异步控制流的良好支持,且数据可流动,当前线程AsyncLocal实例所存储的数据可流动到异步任务控制流中的默认任务调度线程池的线程中
固然咱们也能够调用以下执行环境上下文中的抑制流动方法来禁用数据流动
private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
asyncLocal.Value = "asyncLocal";
using (ExecutionContext.SuppressFlow())
{
Task.Run(() =>
{
Console.WriteLine($"Inside child task: {asyncLocal.Value}");
}).Wait();
}
Console.WriteLine($"after await:{asyncLocal.Value}");
Console.ReadLine();
}
此时在其任务内部打印的值将为空。最后,咱们再来对AsyncLocal作一个最终总结