前言

最近查看有关框架源码,发现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. java Excel 自动调整列宽

    在开发中经常需要用到对Excel文件的操作,现在根据网上的资料整理如下: import java.io.FileOutputStream; import org.apache.poi.hssf.use ...

  2. Flutter源码剖析(一):源码获取与构建

    概述 本文介绍了Flutter源码的获取与构建,后面会另有文章介绍Flutter源码的版本管理.开发环境搭建等主题. 准备工作 Flutter源码分为两个部分: flutter/flutter是框架层 ...

  3. ASP.NET Core Authentication系列(二)实现认证、登录和注销

    前言 在上一篇文章介绍ASP.NET Core Authentication的三个重要概念,分别是Claim, ClaimsIdentity, ClaimsPrincipal,以及claims-bas ...

  4. 4g物联网模块的原理

    4G DTU模块也可以被称之为是含有第四代移动通信技术的模块,是随着科技不断发展进步下物联网和移动互联网发展下的又一产物.而4G技术包括TD-LTE和FDD-LTE两种制式.集3G与WLAN于一体并能 ...

  5. linux用户及组相关文件介绍

    用户和组文件介绍 1.用户账号文件:passwd passwd命令用于设置用户的认证信息,包括用户密码.密码过期时间等.系统管理者则能用它管理系统用户的密码.只有管理者可以指定用户名称,一般用户只能变 ...

  6. [P2114] [NOI2014]起床困难综合症 (位运算)

    题面 传送门:https://www.luogu.org/problemnew/show/P2114 Solution 一道很有意思的位运算题. 要做这一题,我们首先得了解一个很重要的特点 位运算过程 ...

  7. Charles使用part1——基本功能介绍

    一. 安装与破解: 官网地址:https://www.charlesproxy.com/download/ 破解自行解决. 二. 启动与配置: 启动 Charles 后,第一次 Charles 会请求 ...

  8. 使用Asponse.Words处理Word模板

    一.客户需求 近期接到一个项目,在与客户初步沟通后,客户描述的需求听起来也非常简单,就是目前客户需要在Excel录入数据,然后把这些数据分别复制到多个Word的多个地方,除了单个值之外,还需要复制表格 ...

  9. SpringCloud gateway 过滤

    如果需要获取一张图片但服务器没有过滤图片请求地址时,每次请求图片都需要携带token等安全验证密钥,可到nacos配置网关(gateway)的security配置,可过滤掉你配置的url(可理解为白名 ...

  10. 3、编程语言与Python介绍

    一 引子 基于上一章所学,有了计算机硬件,再在硬件之上安装好操作系统,我们就有了一个应用程序的运行平台,我们接下来的任务就是学习如何使用某款编程语言来开发应用程序. 本章的主题是先了解一下编程语言,然 ...