AsyncLocal是如何实现在Thread直接传值的?

一:背景

1. 讲故事

这个问题的由来是在.NET高级调试训练营第十期分享ThreadStatic底层玩法的时候,有朋友提出了AsyncLocal是如何实现的,虽然做了口头上的表述,但总还是会不具体,所以觉得有必要用文字+图表的方式来系统的说一下这个问题。

二:AsyncLocal 线程间传值

1. 线程间传值途径

在 C# 编程中实现多线程以及线程切换的方式大概如下三种:

  • Thread

  • Task

  • await,async

这三种场景下的线程间传值有各自的实现方式,由于篇幅限制,先从 Thread 开始聊吧。本质上来说 AsyncLocal 是一个纯托管的C#玩法,和 coreclr,Windows 没有任何关系。

2. Thread 小例子

为了方便讲述,先来一个例子看下如何在新Thread线程中提取 _asyncLocal 中的值,参考代码如下:

internal class Program{static AsyncLocal<int> _asyncLocal = new AsyncLocal<int>();static void Main(string[] args){_asyncLocal.Value = 10;var t = new Thread(() =>{Console.WriteLine($"Tid={Thread.CurrentThread.ManagedThreadId}, AsyncLocal value: {_asyncLocal.Value},");Debugger.Break();});t.Start();Console.ReadLine();}}

从截图看 tid=7 线程果然拿到了 主线程设置的 10 ,哈哈,是不是充满了好奇心?接下来逐一分析下吧。

3. 流转分析

首先观察下 _asyncLocal.Value = 10 在源码层做了什么,参考代码如下:

public T Value{set{ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);}}internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications){ExecutionContext executionContext = Thread.CurrentThread._executionContext;Thread.CurrentThread._executionContext = new ExecutionContext(asyncLocalValueMap, array, flag2));}

从源码中可以看到这个 10 最终封印在 Thread.CurrentThread._executionContext 字段中,接下来就是核心问题了,它是如何被送到新线程中的呢?

其实仔细想一想,要让我实现的话,我肯定这么实现。

  1. 将主线程的 _executionContext 字段赋值给新线程 t._executionContext 字段。

  2. 将 var t = new Thread() 中的t作为参数传递给 win32 的 CreateThread 函数,这样在新线程中就可以提取 到 t 了,然后执行 t 的callback。

这么说大家可能有点抽象,我就直接画下C#是怎么流转的图吧:

有了这张图之后接下来的问题就是验证了,首先看一下 copy 操作在哪里? 可以观察下 Start 源码。

private void Start(bool captureContext){StartHelper startHelper = _startHelper;if (startHelper != null){startHelper._startArg = null;startHelper._executionContext = (captureContext ? System.Threading.ExecutionContext.Capture() : null);}StartCore();}public static ExecutionContext? Capture(){ExecutionContext executionContext = Thread.CurrentThread._executionContext;return executionContext;}

从源码中可以看到将主线程的 _executionContext 字段给了新线程t下的startHelper._executionContext 。

接下来我们观察下在创建 OS 线程的时候是不是将 Thread 作为参数传过去了,如果传过去了,那就可以直接在新线程中拿到 Thread._startHelper._executionContext 字段,验证起来也很简单,在win32 的 ntdll!NtCreateThreadEx 上下一个断点即可。


0:000> bp ntdll!NtCreateThreadEx
0:000> g
Breakpoint 1 hit
ntdll!NtCreateThreadEx:
00007ff9`0fe8e8c0 4c8bd1          mov     r10,rcx
0:000> r
rax=00007ff8b4a529d0 rbx=0000000000000000 rcx=0000008471b7df28
rdx=00000000001fffff rsi=0000027f2ca25b01 rdi=0000027f2ca25b60
rip=00007ff90fe8e8c0 rsp=0000008471b7de68 rbp=00007ff8b4a529d0r8=0000000000000000  r9=ffffffffffffffff r10=0000027f2c8a0000
r11=0000008471b7de40 r12=0000008471b7e890 r13=0000008471b7e4f8
r14=ffffffffffffffff r15=0000000000010000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ntdll!NtCreateThreadEx:
00007ff9`0fe8e8c0 4c8bd1          mov     r10,rcx
0:000> !t
ThreadCount:      4
UnstartedThread:  1
BackgroundThread: 2
PendingThread:    0
DeadThread:       0
Hosted Runtime:   noLock  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception0    1     2cd8 0000027F2C9E6610    2a020 Preemptive  0000027F2E5DB438:0000027F2E5DB4A0 0000027f2c9dd670 -00001 MTA 6    2     2b24 0000027F2CA121E0    21220 Preemptive  0000000000000000:0000000000000000 0000027f2c9dd670 -00001 Ukn (Finalizer) 7    3     2658 0000027F4EAA0AE0    2b220 Preemptive  0000000000000000:0000000000000000 0000027f2c9dd670 -00001 MTA 
XXXX    4        0 0000027F2CA25B60     9400 Preemptive  0000000000000000:0000000000000000 0000027f2c9dd670 -00001 Ukn 

从输出中可以看到 NtCreateThreadEx 方法的第二个参数即 rdi=0000027f2ca25b60 就是我们的托管线程,如果你不相信的话可以再用 windbg 找到它的托管线程信息,输出如下:


0:000> dt coreclr!Thread 0000027F2CA25B60 -y m_ExposedObject+0x1c8 m_ExposedObject : 0x0000027f`2c8f11d0 OBJECTHANDLE__0:000> !do poi(0x0000027f`2c8f11d0)
Name:        System.Threading.Thread
MethodTable: 00007ff855090d78
EEClass:     00007ff85506a700
Tracked Type: false
Size:        72(0x48) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.25\System.Private.CoreLib.dll
Fields:MT    Field   Offset                 Type VT     Attr            Value Name
00007ff8550c76d8  4000b35        8 ....ExecutionContext  0 instance 0000000000000000 _executionContext
0000000000000000  4000b36       10 ...ronizationContext  0 instance 0000000000000000 _synchronizationContext
00007ff85508d708  4000b37       18        System.String  0 instance 0000000000000000 _name
00007ff8550cb9d0  4000b38       20 ...hread+StartHelper  0 instance 0000027f2e5db3b0 _startHelper
...

有些朋友可能要说,你现在的 _executionContext 字段是保留在 _startHelper 类里,并没有赋值到Thread._executionContext字段呀?那这一块在哪里实现的呢?从上图可以看到其实是在新线程的执行函数上,在托管函数执行之前会将 _startHelper._executionContext 赋值给 Thread._executionContext , 让 windbg 继续执行,输出如下:


0:009> k# Child-SP          RetAddr               Call Site
00 00000084`728ff778 00007ff8`b4c23d19     KERNELBASE!wil::details::DebugBreak+0x2
01 00000084`728ff780 00007ff8`b43ba7ea     coreclr!DebugDebugger::Break+0x149 [D:\a\_work\1\s\src\coreclr\vm\debugdebugger.cpp @ 148] 
02 00000084`728ff900 00007ff8`54ff56e3     System_Private_CoreLib!System.Diagnostics.Debugger.Break+0xa [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18] 
03 00000084`728ff930 00007ff8`b42b4259     ConsoleApp9!ConsoleApp9.Program.<>c.<Main>b__1_0+0x113
04 00000084`728ff9c0 00007ff8`b42bddd9     System_Private_CoreLib!System.Threading.Thread.StartHelper.Callback+0x39 [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs @ 42] 
05 00000084`728ffa00 00007ff8`b42b2f4a     System_Private_CoreLib!System.Threading.ExecutionContext.RunInternal+0x69 [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 183] 
06 00000084`728ffa70 00007ff8`b4b7ba53     System_Private_CoreLib!System.Threading.Thread.StartCallback+0x8a [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 105] 
07 00000084`728ffab0 00007ff8`b4a763dc     coreclr!CallDescrWorkerInternal+0x83
08 00000084`728ffaf0 00007ff8`b4b5e713     coreclr!DispatchCallSimple+0x80 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 220] 
09 00000084`728ffb80 00007ff8`b4a52d25     coreclr!ThreadNative::KickOffThread_Worker+0x63 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 158] 
...
0d (Inline Function) --------`--------     coreclr!ManagedThreadBase_FullTransition+0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7569] 
0e (Inline Function) --------`--------     coreclr!ManagedThreadBase::KickOff+0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7604] 
0f 00000084`728ffd60 00007ff9`0e777614     coreclr!ThreadNative::KickOffThread+0x79 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 230] 
10 00000084`728ffdc0 00007ff9`0fe426a1     KERNEL32!BaseThreadInitThunk+0x14
11 00000084`728ffdf0 00000000`00000000     ntdll!RtlUserThreadStart+0x21
...

在上面的回调函数中看的非常清楚,在执行托管函数 <Main>b__1_0 之前执行了一个 ExecutionContext.RunInternal 函数,对,就是它来实现的,参考代码如下:

private sealed class StartHelper{internal void Run(){System.Threading.ExecutionContext.RunInternal(_executionContext, s_threadStartContextCallback, this);}}internal static void RunInternal(ExecutionContext executionContext, ContextCallback callback, object state){Thread currentThread = Thread.CurrentThread;RestoreChangedContextToThread(currentThread, executionContext, executionContext3);}internal static void RestoreChangedContextToThread(Thread currentThread, ExecutionContext contextToRestore, ExecutionContext currentContext){currentThread._executionContext = contextToRestore;}

既然将 StartHelper.executionContext 塞到了 currentThread._executionContext 中,在 <Main>b__1_0 方法中自然就能通过 _asyncLocal.Value 提取了。

三:总结

说了这么多,其实精妙之处在于创建OS线程的时候,会把C# Thread实例(coreclr对应线程) 作为参数传递给新线程,即下面方法签名中的 lpParameter 参数,新线程拿到了Thread实例,自然就能获取到调用线程赋值的 Thread._executionContext 字段,所以这是完完全全的C#层面玩法,希望能给后来者解惑吧!


HANDLE CreateThread([in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,[in]            SIZE_T                  dwStackSize,[in]            LPTHREAD_START_ROUTINE  lpStartAddress,[in, optional]  __drv_aliasesMem LPVOID lpParameter,[in]            DWORD                   dwCreationFlags,[out, optional] LPDWORD                 lpThreadId
);

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

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

相关文章

百度智能小程序开发平台:SEO关键词推广优化 带完整的搭建教程

移动互联网的普及&#xff0c;小程序成为了众多企业和开发者关注的焦点。百度智能小程序开发平台为开发者提供了一站式的解决方案&#xff0c;帮助企业快速搭建并推广自己的小程序。本文将重点介绍百度智能小程序开发平台的SEO关键词推广优化功能&#xff0c;并带完整的搭建教程…

img标签插入图片下方有空隙,怎么解决?

在写静态页面时&#xff0c;经常会用 img 标签插入图片&#xff0c;但图片插入后&#xff0c;直接在浏览器中运行时&#xff0c;图片的下方经常会有空隙间距&#xff0c;文字或者其它元素不好跟图片对齐&#xff0c;devtools 工具查看的话会很明显。 如下图&#xff1a; 上图…

Coppeliasim倒立摆demo

首先需要将使用Python远程控制的文件导入到文件夹&#xff0c;核心是深蓝色的三个文件。 本版本为4.70&#xff0c;其文件所在位置如下图所示&#xff0c;需要注意的是&#xff0c;目前不支持Ubuntu22的远程api&#xff1a; 双击Sphere这一行的灰色文件&#xff0c;可以看到远程…

【Docker】【深度学习算法】在Docker中使用gunicorn启动多个并行算法服务,优化算法服务:从单进程到并行化

文章目录 优化算法服务&#xff1a;从单进程到并行化单个服务架构多并行服务架构Docker化并指定并行服务数量 优化算法服务&#xff1a;从单进程到并行化 在实际应用中&#xff0c;单个算法服务的并发能力可能无法满足需求。为了提高性能和并发处理能力&#xff0c;我们可以使…

AI 神助攻,协同办公神器 ---- ONLYOFFICE

人工智能不会取代人&#xff0c;只会淘汰那些不会使用人工智能的人。 – 鲁迅 一、人工智能重新定义办公新模式 随着GPT的横空出世&#xff0c;AI的应用场景已经无处不在&#xff0c;从智能客服、智能语音助手、智能家居到自动驾驶汽车等&#xff0c;AI正在不断地拓展其应用领…

Orion-14B-Chat-Plugin [model server error]解决方案

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

【大数据安全】大数据安全的挑战与对策基础设施安全

目录 一、大数据安全的挑战与对策 &#xff08;一&#xff09;数据加密技术 &#xff08;二&#xff09;大数据安全与隐私 &#xff08;三&#xff09;大数据安全保障体系 &#xff08;四&#xff09;华为大数据安全解决方案 二、基础设施安全 &#xff08;一&#xff0…

AI-数学-高中-12-对数定义和基本运算规则、对数换底公式

原作者视频&#xff1a;初等函数】4对数定义基本运算规则&#xff08;基础&#xff09;_哔哩哔哩_bilibili 初等函数】5对数换底公式练习&#xff08;基础&#xff09;_哔哩哔哩_bilibili 对数读法&#xff1a;以a为底&#xff0c;b的对数&#xff0c;a为底数&#xff0c;b为…

KEIL指定文件夹生成:程序名字+版本号+时间的脚本

为了免去每次更新版本还需要手动更改可执行程序的名字繁琐步骤。特此参考网上一个开源脚本编写了一个脚本文件。直接上图&#xff1a; 1.在指定的文件中填写&#xff1a;#define SOFTWARE_VERSION "1.0.2" 2.将脚本添加到如下图所示位置 3.点击编译&#xff0c;得到…

vxe-table3.0的表格树如何做深层查找,返回搜索关键字的树形结构

vxe-table2.0版本是提供深层查找功能的&#xff0c;因为他的数据源本身就是树形结构&#xff0c;所以深层查找查询出来也是树形结构。 但是vxe-table3.0版本为了做虚拟树功能&#xff0c;将整个数据源由树形垂直结构变成了扁平结构&#xff0c;便不提供深层查询功能&#xff0c…

【PWN · ret2syscall】[CISCN 2023 初赛]烧烤摊儿

好久未做ret2syscall 一、题目分析 漏洞点&#xff0c;栈溢出&#xff1a; 然而到该漏洞点&#xff0c;需要“花钱”买下店铺。个人所有的零钱不够。 利用整数溢出&#xff08;emmmm这里应该不算溢出漏洞&#xff0c;而是代码逻辑不规范&#xff0c;商品个数不能为负数&#x…

华为数通方向HCIP-DataCom H12-831题库(简答题01-27)

第01题 第02题 第03题 第04题 第05题 IS-IS是链路状态路由协议,使用SPF算法进行路由计算。某园区同时部署了IPV4和IPv6并运行IS-IS实现网络的互联与通。如图所示,该网络IPV4和IPV6开销相同,R1和R4只支持IPV4缺省情况下,计算形成的IPV6最短路径树中,R2访问R6的下一跳设备是…

uniapp H5 实现上拉刷新 以及 下拉加载

uniapp H5 实现上拉刷新 以及 下拉加载 1. 先上图 下拉加载 2. 上代码 <script>import DragableList from "/components/dragable-list/dragable-list.vue";import {FridApi} from /api/warn.jsexport default {data() {return {tableList: [],loadingHi…

【JAVA】Semaphore 有什么作用

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 1. 二进制信号量&#xff1a; 2. 计数信号量&#xff1a; 结语 我的其他博客 前言 Semaphore&#xff08;信号量&#xff09;作为…

uniapp微信小程序-分包

一、为什么要分包 微信小程序每个分包的大小是2M&#xff0c;总体积一共不能超过20M,当然你也可以提升启动速度&#xff0c;降低首次加载时间&#xff0c;模块化开发&#xff0c;按需加载&#xff0c;提高性能。 二、分包步骤 1.首先在 mainfest.json mp-weixin添加以下代码&a…

idea创建golang项目

目录 1、设置环境 2、创建项目 3、设置项目配置 4、初始化项目 5、安装本项目的外部依赖包 6、运行项目 7、访问页面查看结果 1、设置环境 1 启用 Go Modules 功能go env -w GO111MODULEon 2. 阿里云go env -w GOPROXYhttps://mirrors.aliyun.com/goproxy/,direct上述命…

字符串:getline、删除子串.erase()函数、插入子串.insert()函数

getline具体用法&#xff1a; 1、函数形式 getline ( cin,字符串类型&#xff1a;变量名);//默认以换行符结束 getline (cin, 字符串类型&#xff1a;变量名, ‘指定的结束符’); //指定换行结束符 2注意事项&#xff1a; 1&#xff09;如果在使用getline()之前有使用scanf(…

大数据分析案例-基于随机森林算法构建电影票房预测模型

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

机房建设的陷阱与规范

在数字化时代&#xff0c;数据中心的建设变得愈发重要&#xff0c;然而&#xff0c;许多集成商在机房建设中存在一些常见的误区。他们往往将机房建设仅仅定义为能源设备的安装和装修&#xff0c;而忽略了甲方的网络应用、服务器应用以及网络与服务器之间的连接应用。这导致了一…

前端-Vue項目初始化

大家好我是苏麟 , 今天聊聊前端依赖 Ant Design Vue 快速初始化项目 . Ant Design Vue官网 : 快速上手 - Ant Design Vue (antdv.com) 初始化项目 1.找到文档->快速上手 脚手架命令 : npm install -g vue/cli 2.找到一个文件夹(不要在中文路径) 下打开cmd窗口输入脚手架…