浅谈AsyncLocal,我们应该知道的那些事儿

【导读】最近查看有关框架源码,发现AsyncLocal这玩意水还挺深,于是花了一点功夫去研究,同时对比ThreadLocal说明二者区别以及在何时场景下使用AsyncLocal或ThreadLocal

ThreadLocal相信很多童鞋用过,但AsyncLocal具体使用包括我在内的一大部分童鞋应该完全没怎么使用过。

AsyncLocal同样出现在.NET Framework 4.6+(包括4.6),当然在.NET Core中没有版本限制即CoreCLR,对此类官方所给的解释是:将本地环境数据传递到异步控制流,例如异步方法

又例如缓存WCF通信通道,可以使用AsyncLocal而不是.NET Framework或CoreCLR所提供的ThreadLocal

官方概念解释在我们初次听来好像还是有点抽象,不打紧,接下来我们通过实际例子来进行详细说明和解释

AsyncLocal和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进行异步控制流时,由于内部对数据进行浅拷贝,确保其实例类型参数应为不可变数据类型

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306613.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法设计与分析——动态规划——数字三角形问题

数字三角形问题 1.题目描述&#xff1a;给定一个由n行数字组成的数字三角形&#xff0c;如图3-7所示。设计一个算法&#xff0c;计算出从三角形的顶至底的一条路径&#xff0c;使该路径经过的数字总和最大。 算法设计:对于给定的由n行数字组成的数字三角形&#xff0c;计算从三…

如何在 Asp.Net Core MVC 中处理 null 值

译文链接&#xff1a;https://www.infoworld.com/article/3434624/how-to-handle-null-values-in-aspnet-core-mvc.html传统的 asp.net mvc 对应着 .netcore 中的 asp.net core mvc&#xff0c;可以利用 asp.net core mvc 去构建跨平台&#xff0c;可扩展&#xff0c;高性能的w…

算法设计与分析——动态规划——最长公共子序列

#include<iostream> #include<stdio.h> #include<string.h> #include<bits/stdc.h> #define MAXLEN 50 using namespace std;void LCSlength(int m,int n,char *x,char *y,int c[][MAXLEN],int b[][MAXLEN]) {for(int i0;i<m;i)//m为字符个数 {c[i]…

程序员过关斩将--论系统设计的高可扩展性

“此文仅仅代表个人意见&#xff0c;并非行业标准“MQ是万能的高扩展方式&#xff1f;“面向接口是万能的高扩展方式&#xff1f;说到系统设计的三高&#xff0c;每一高都是一个很庞大的话题&#xff0c;甚至可以用一本书甚至N本书来详细阐述。其中高可扩展性是系统架构的众多目…

Docker Vs Podman

翻译自 Chetansingh 2020年4月24日的博文《Docker Vs Podman》 [1]容器化的一场全新革命是从 Docker 开始的&#xff0c;Docker 的守护进程管理着所有的事情&#xff0c;并成为最受欢迎和广泛使用的容器管理系统之一。但是&#xff0c;请稍等&#xff01;您真的会假设 Docker 是…

算法设计与分析——动态规划——01背包问题

#include<iostream> #include<iomanip> using namespace std; //前i个物品装入容量为j的背包中获得的最大价值//0-1背包动态规划算法 构造二维表 int knapsack_problem( int n,int *weight,int *value,int capacity,int **m,int *flag) {for(int i0;i<capaci…

让 CefSharp.WinForms 应用程序同时支持32位(x86)和64位(x64)的解决方案

当我们为基于 .NET Framework 的 WinForm 程序增加 CefSharp.WinForms 依赖后&#xff0c;可能会遇到以下报错信息&#xff1a;CefSharp.Common is unable to proceeed as your current Platform is ‘AnyCPU’. To target AnyCPU please read https://github.com/cefsharp/Cef…

算法设计与分析——贪心算法——活动安排问题

问题描述&#xff1a;设有n个活动的集合E{1,2,…,n}&#xff0c;其中每个活动都要求使用同一资源&#xff0c;如演讲会场等&#xff0c;而在同一时间内只有一个活动能使用这一资源。 每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi 。 如果选择了活动…

c语言——刷新控制台界面与返回上一级

#include<iostream> #include<stdlib.h> using namespace std;int main() {while(1){system("cls");//刷新控制台程序界面 cout<<"请输入1&#xff1a;进入下一级程序A:"<<endl;cout<<"请输入2&#xff1a;进入下一级程…

C# :异步编程的注意点

在上一篇《C#&#xff1a;异步编程中的 async 和 await》 中简单介绍了在 C# 中的异步编程以及 async 和 await 编程模型&#xff0c;本文介绍下异步编程的注意事项&#xff0c;主要有以下几个方面。同步中调用异步在同步代码中调用异步代码&#xff0c;容易导致死锁&#xff0…

makefile 打印变量_[Makefile] 缩进与空格--记录踩过的坑

今天折腾了好久&#xff0c;就为了debug两个makefile的bug。虽然最后找到原因了&#xff0c;但是&#xff0c;怎么说呢&#xff0c;用现在流行的话来说&#xff0c;实在是意难平啊&#xff01;必须写一篇记录一下&#xff01;第一个问题&#xff0c;是个语法高亮问题。今天观察…

算法设计与分析——贪心算法——背包问题

0-1背包问题&#xff1a; 前提&#xff1a;给定n种物品和一个背包。物品i的重量是Wi&#xff0c;其价值为Vi&#xff0c;背包的容量为C。 问题&#xff1a;应如何选择装入背包的物品&#xff0c;使得装入背包中物品的总价值最大? 背包问题&#xff1a; 与0-1背包问题类似&…

企业级精致 Blazor 套件 BootstrapBlazor 介绍

BootstrapBlazor1、前言 Blazor 作为一种 Web 开发的新技术已经发展有一段时间了&#xff0c;有些人标称 无 JS 无 TS&#xff0c;我觉得有点误导新人的意味&#xff0c;也有人文章大肆宣传 Blazor 是 JavaScript 的终结者&#xff0c;是为了替代 JavaScript 而生的&#xff0c…

算法设计与分析——贪心算法——最优装载问题

有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下&#xff0c;将尽可能多的集装箱装上轮船。 #include<iostream> #include<algorithm> #include<cstring> using namespace std;typedef str…

AgileConfig-轻量级配置中心 1.1.0 发布,支持应用间配置继承

AgileConfig轻量级配置中心自第一个版本发布不知不觉已经半年了。在并未进行什么推广的情况下收到了250个star&#xff0c;对我有很大的鼓舞&#xff0c;并且也有不少同学试用&#xff0c;并且给出了宝贵的意见&#xff0c;非常感谢他们。其中有一些意见非常好&#xff0c;但是…

如何在 C# 中使用 Dapper ORM

译文链接&#xff1a;https://www.infoworld.com/article/3025784/how-to-use-the-dapper-orm-in-c.html?nsdrtrue对象关系映射&#xff08;ORM&#xff09;这个概念已经存在很长时间了&#xff0c;ORM的作用就是用来解决 编程领域的 object model 和关系数据库中的 data mode…

GraphQL:简单开开始一个查询

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述&#xff0c;使得客户端能够准确地获得它需要的数据&#xff0c;而且没有任何冗余&#xff0c;也让 API 更容易地随着时间推移而演进&#xff0c…

算法设计与分析——回溯法——批处理作业调度

问题描述&#xff1a;给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理&#xff0c;然后由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度&#xff0c;设Fji是作业i在机器j上完成处理的时间。所有作业在机器2上完成处理的时间和称为该作业调度…

探索.NET平台中的SIMD内在函数Vector

概述Vector&#xff08;向量&#xff09;是一种序列式容器&#xff0c;事实上和数组差不多&#xff0c;但它比数组更优越。一般来说数组不能动态拓展&#xff0c;因此在程序运行的时候不是浪费内存&#xff0c;就是造成越界。而Vector刚好弥补了这个缺陷&#xff0c;它的特征是…

算法设计与分析——回溯法——装载问题

0027算法笔记——【回溯法】回溯法与装载问题 自己写的代码&#xff1a; #include <iostream> using namespace std; template <class Type> class Loading {//friend Type MaxLoading(Type[],Type,int,int []);//private:public:void Backtrack(int i);int n, …