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 字段中,接下来就是核心问题了,它是如何被送到新线程中的呢?
其实仔细想一想,要让我实现的话,我肯定这么实现。
-
将主线程的 _executionContext 字段赋值给新线程 t._executionContext 字段。
-
将
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
);
相关文章:
AsyncLocal是如何实现在Thread直接传值的?
一:背景 1. 讲故事 这个问题的由来是在.NET高级调试训练营第十期分享ThreadStatic底层玩法的时候,有朋友提出了AsyncLocal是如何实现的,虽然做了口头上的表述,但总还是会不具体,所以觉得有必要用文字图表的方式来系统…...
Flask 入门1:一个简单的 Web 程序
1. 关于 Flask Flask诞生于2010年, Armin Ronacher的一个愚人节玩笑。不过现在已经是一个用python语言基于Werkzeug工具箱编写的轻量级web开发框架,它主要面向需求简单,项目周期短的小应用。 Flask本身相当于一个内核,其他几乎所…...
维护管理Harbor,docker容器的重启策略
维护管理Harbor 通过HarborWeb创建项目 在 Harbor 仓库中,任何镜像在被 push 到 regsitry 之前都必须有一个自己所属的项目。 单击“项目”,填写项目名称,项目级别若设置为"私有",则不勾选。如果设置为公共仓库&#…...
Qt6入门教程 14:QToolButton
目录 一.简介 二.常用接口 1.void setMenu(QMenu * menu) 2.void setPopupMode(ToolButtonPopupMode mode) 3.void setToolButtonStyle(Qt::ToolButtonStyle style) 4.void setArrowType(Qt::ArrowType type) 5.void setDefaultAction(QAction * action) 三.实战演练 1…...
3D数据转换器HOOPS Exchange如何获取模型的几何数据? 干货预警!
一、概述 前面讲解过模型在内存中的结构,现在回顾一下,当模型导入成功后,整个模型数据会以原生结构的 PRC 组装树形式存放到内存中。(申请 HOOPS Exchange 试用) PRC结构的主要类型包含四种,分别是…...
Coremail启动鸿蒙原生应用开发,打造全场景邮件办公新体验
1月18日,华为在深圳举行鸿蒙生态千帆启航仪式,Coremail出席仪式并与华为签署鸿蒙合作协议,宣布正式启动鸿蒙原生应用开发。作为首批拥抱鸿蒙的邮件领域伙伴,Coremail的加入标志着鸿蒙生态版图进一步完善。 Coremail是国内自建邮件…...
基于CVITEK_CV1821+SOI_Q03P的IPC方案
方案概述: 该方案基于主控平台CVITEK_CV1821和sensor SOI_Q03P,运用于智能监控IP摄像头,可用于户外或室内。采用了2304x1296的分辨率,30的帧率,支持HDR。作为3M的监控摄像头,通过ISP图像调校技术ÿ…...
chromedriver安装和环境变量配置
chromedriver 1、安装2、【重点】环境变量配置(1)包的复制:(2)系统环境变量配置 3、验证 1、安装 网上随便搜一篇chromedriver的安装文档即可。这里是一个快速链接 特别提醒:截止2024.1.30,chr…...
Linux浅学笔记03
目录 有关root的命令 用户和用户组 用户组管理:(以下需要root用户执行) 创建用户组: 删除用户组: 用户管理:(以下需要root用户执行) 创建用户: 删除用户: 查看用…...
【vue】图片加载骨架
一、前言 在网速较低或者网站的服务器宽带只有几MB的情况下,网页中的图片加载时,要么空白,要么像打印机一样一行一行地“扫描”出来,为了提升用户体验,可以给图片标签外加一层骨架。 无骨架 有骨架 二、详细设计 每张…...
leetcode59. 螺旋矩阵 II
leetcode59. 螺旋矩阵 II 题目 思路 螺旋数组,一次螺旋4个方向(上行从左到右、右列从上到下、下行从右到左、左列从下到上),共执行(n//2)次螺旋。且对于n为奇数时,额外填充中心点nums[mid][mid] n 每一次螺旋圈下来…...
bash 5.2中文修订5
Grouping Commands 命令分组 Bash 提供两种方法将要执行的命令列表分组为一个单元。当命令被分组时,重定向可以应用于整个命令列表。例如,列表中所有命令的输出可以被重定向到单个流。 () 圆括号命令分组 ( list ) 将命令列表放在括号之间会强制 she…...
5GNR解调分析手持式频谱分析仪
2024年已经是5G网络全面普及的一年,手机也基本都升级了5G版本,那么同样的,5G的网络运行也是需要维护的。 我们知道,5G是新型的网络传输技术,如果一般的频谱分析仪是没有办法单独针对5G NR进行解析的。这个时候你就需要…...
互联网加竞赛 基于深度学习的人脸表情识别
文章目录 0 前言1 技术介绍1.1 技术概括1.2 目前表情识别实现技术 2 实现效果3 深度学习表情识别实现过程3.1 网络架构3.2 数据3.3 实现流程3.4 部分实现代码 4 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 基于深度学习的人脸表情识别 该项目较…...
python-自动化篇-运维-监控-简单实例-道出如何使⽤Python进⾏网络监控?
如何使⽤Python进⾏⽹络监控? 使⽤Python进⾏⽹络监控可以帮助实时监视⽹络设备、流量和服务的状态,以便及时识别和解决问题。 以下是⼀般步骤,说明如何使⽤Python进⾏⽹络监控: 选择监控⼯具和库:选择适合⽹络监控需…...
SpringBoot 配置类解析
全局流程解析 配置类解析入口 postProcessBeanDefinitionRegistry逻辑 processConfigBeanDefinitions逻辑 执行逻辑解析 执行入口 ConfigurationClassPostProcessor.processConfigBeanDefinitions()方法中的do while循环体中 循环体逻辑 parse方法调用链 doProcessConfigurat…...
全套军事和民用监听系统
Python全套军事和民用监听系统的研发开发具有重要性的原因如下: 监听系统在军事和民用领域中具有广泛的应用。军事方面,监听系统可用于收集敌方情报、监测通信网络、进行电子战等,对于提高作战效能和获取情报优势至关重要。民用方面ÿ…...
MicroPython核心:编译器
MicroPython编译过程包括以下步骤: 词法分析器将MicroPython程序文本流转换为标记。语法解释器将标记转换为抽象语法(语法树)。根据语法书输出字节码或本地代码。 本文以给MicroPython增加一个简单的语言特性为例来说明这一过程:…...
R语言【taxlist】——tax2traits():将分类信息设置为分类单元特征
Package taxlist version 0.2.4 Description 分类法分类可以包含在taxonRelations插槽提供的信息中的 taxlist 对象中。然而,对于统计分析来说,将这些信息插入到插槽taxonTraits中可能更方便。 Usage tax2traits(object, ...)## S3 method for class …...
CTF-WEB的知识体系
CTF概念 CTF是Capture The Flag的缩写,中文一般译作夺旗赛 CTF起源于1996年DEFCON全球黑客大会 DEFCONCTF是全球技术水平和影响力最高的CTF竞赛 竞赛模式 解题模式:解决网络安全技术挑战(即找到flag),提交后获取相应分值。 攻防赛模式:要求找到其他队…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...
MySQL体系架构解析(三):MySQL目录与启动配置全解析
MySQL中的目录和文件 bin目录 在 MySQL 的安装目录下有一个特别重要的 bin 目录,这个目录下存放着许多可执行文件。与其他系统的可执行文件类似,这些可执行文件都是与服务器和客户端程序相关的。 启动MySQL服务器程序 在 UNIX 系统中,用…...
验证redis数据结构
一、功能验证 1.验证redis的数据结构(如字符串、列表、哈希、集合、有序集合等)是否按照预期工作。 2、常见的数据结构验证方法: ①字符串(string) 测试基本操作 set、get、incr、decr 验证字符串的长度和内容是否正…...
五、jmeter脚本参数化
目录 1、脚本参数化 1.1 用户定义的变量 1.1.1 添加及引用方式 1.1.2 测试得出用户定义变量的特点 1.2 用户参数 1.2.1 概念 1.2.2 位置不同效果不同 1.2.3、用户参数的勾选框 - 每次迭代更新一次 总结用户定义的变量、用户参数 1.3 csv数据文件参数化 1、脚本参数化 …...
C#最佳实践:为何优先使用as或is而非强制转换
C#最佳实践:为何优先使用as或is而非强制转换 在 C# 的编程世界里,类型转换是我们经常会遇到的操作。就像在现实生活中,我们可能需要把不同形状的物品重新整理归类一样,在代码里,我们也常常需要将一个数据类型转换为另…...
