前言

最近查看有关框架源码,发现AsyncLocal这玩意水还挺深,于是花了一点功夫去研究,同时对比ThreadLocal说明二者区别以及在何时场景下使用AsyncLocal或ThreadLocal。ThreadLocal相信很多童鞋用过,但AsyncLocal具体使用包括我在内的一大部分童鞋应该完全没怎么使用过。

AsyncLocal和ThreadLocal区别

AsyncLocal同样出现在.NET Framework 4.6+(包括4.6),当然在.NET Core中没有版本限制即CoreCLR,对此类官方所给的解释是:将本地环境数据传递到异步控制流,例如异步方法。又例如缓存WCF通信通道,可以使用AsyncLocal而不是.NET Framework或CoreCLR所提供的ThreadLocal。官方概念解释在我们初次听来好像还是有点抽象,不打紧,接下来我们通过实际例子来进行详细说明和解释,首先我们先看如下例子,然后再分析二者和什么有关系

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();
}

猜猜如上将会打印出什么结果呢?

为何ThreadLocal所打印的值为空值呢?我们不是设置了值吗?此时我们将要从执行环境开始说起。若完全理解ExecutionContext与SynchronizationContext二者概念和关系,理论上来讲则可解答出上述问题,这里我们简单叙述下,更详细介绍请查阅相关资料自行了解ExecutionContext俗称“执行上下文”,也就是说和“环境”信息相关,这也就意味着它存储着和我们当前程序所执行的环境相关的数据,这类环境信息数据存储在ThreadStatic或ThreadLocal中,换句话说ThreadLocal和特定线程相关。上述我们讨论的是相同环境或上下文中,若是不同上下文即不同线程中,那情况又该如何呢?在异步操作中,在某一个线程中启动操作,但却在另一线程中完成,此时我们将不能利用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做一个最终总结

总结

AsyncLocal出现于.NET Framework 4.6+(包含4.6)、CoreCLR

AsyncLocal是每个ExecutionContext实例的一个变量,它并非如同ThreadLocal基于特定线程的持久化数据存储

若需要基于本地环境的异步控制流,使用AsyncLocal而非ThreadLocal,在线程池中重用线程时,ThreadLocal会保留之前值(基于理论猜测),而AsyncLocal不会

AsyncLocal在每次asyn/await后,都将重新生成一个新的异步执行上下文环境,父异步上下文执行环境和子异步上下文执行环境完全隔离且互不影响

AsyncLocal进行异步控制流时,由于内部对数据进行浅拷贝,确保其实例类型参数应为不可变数据类型

浅谈AsyncLocal,我们应该知道的那些事儿的更多相关文章

  1. 浅谈 Fragment 生命周期

    版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...

  2. 浅谈 LayoutInflater

    浅谈 LayoutInflater 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/View 文中如有纰漏,欢迎大家留言指出. 在 Android 的 ...

  3. 浅谈Java的throw与throws

    转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...

  4. 浅谈SQL注入风险 - 一个Login拿下Server

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

  5. 浅谈WebService的版本兼容性设计

    在现在大型的项目或者软件开发中,一般都会有很多种终端, PC端比如Winform.WebForm,移动端,比如各种Native客户端(iOS, Android, WP),Html5等,我们要满足以上所 ...

  6. 浅谈angular2+ionic2

    浅谈angular2+ionic2   前言: 不要用angular的语法去写angular2,有人说二者就像Java和JavaScript的区别.   1. 项目所用:angular2+ionic2 ...

  7. iOS开发之浅谈MVVM的架构设计与团队协作

    今天写这篇博客是想达到抛砖引玉的作用,想与大家交流一下思想,相互学习,博文中有不足之处还望大家批评指正.本篇博客的内容沿袭以往博客的风格,也是以干货为主,偶尔扯扯咸蛋(哈哈~不好好工作又开始发表博客啦 ...

  8. Linux特殊符号浅谈

    Linux特殊字符浅谈 我们经常跟键盘上面那些特殊符号比如(?.!.~...)打交道,其实在Linux有其独特的含义,大致可以分为三类:Linux特殊符号.通配符.正则表达式. Linux特殊符号又可 ...

  9. 浅谈Angular的 $q, defer, promise

    浅谈Angular的 $q, defer, promise 时间 2016-01-13 00:28:00  博客园-原创精华区 原文  http://www.cnblogs.com/big-snow/ ...

随机推荐

  1. 如何在Debian上安装和使用PHP Composer

    1.条件 shell使用sudo权限访问正在运行的debian系统. 必须安装和配置5.3或更高版本的PHP. 2.在Debian上安装Composer 可以通过运行以下命令从getcomposer. ...

  2. 面试小问题——Object中有哪些常用方法?

    一.equals方法 Object类中的equals方法用于检测一个对象是否等于另外一个对象.Java语言规范要求equals方法具有下面的特性: (1)自反性:对于任何非空引用x,x.equals( ...

  3. 测试流程规范--测试准入、准出、停止标准、bug优先级定义

    一.背景 最近在推进组内流程规范专项建设,从"研发测试流程"."提测规范"."测试准入标准"."bug优先级标准".&q ...

  4. python开发基础(二)运算符以及数据类型之dict(字典)

    # encoding: utf-8 # module builtins # from (built-in) # by generator 1.147 """ Built- ...

  5. JavaSE基础语法学习-异常

    异常 异常指程序运行中出现的不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等. 异常发生在程序运行期间,它影响了正常的程序执行流程. 比如说,你的代码少了一个分号,那么运行出来结果是提示是 ...

  6. Python练手项目实例汇总(附源码下载)

    今天给大家分享几个有趣的Python练手项目实例,希望对Python初学者有帮助哈~ 一.经典的俄罗斯方块   1. 绑定功能 1 # 绑定功能 2 class App(Frame): 3 def _ ...

  7. 转载:Pycharm的常用快捷键

    一直想着找一下pycharm的快捷键,但是每次都忘记找了,这次刚好碰到一个很全的,就直接借用别人的来当作自己的笔记ba 转载来源:https://www.cnblogs.com/liangmingsh ...

  8. 后端狗的Vue学习历程(一) - demo示例与基本逻辑语法

    目录 demo的三部分结构 判断:v-if.v-else-if.v-else 循环:v-for 事件绑定 v-on:eventType 内容输入的双向绑定v-model 源码:Github demo的 ...

  9. Electron入门指北

    最近几年最火的桌面化技术,无疑是Qt+和Electron. 两者都有跨平台桌面化技术,并不局限于Windows系统.前者因嵌入式而诞生,在演变过程中,逐步完善了生态以及工具链.后者则是依托于Node. ...

  10. .NET必知的EventCounters性能指标监视器

    在.NET我们对于性能指标监控,其实常见的有两个方法,一个是CLI工具dotnet-counters而另一个是代码级别的EventListener. 使用dotnet-counters dotnet- ...