C# AsyncLocal 是如何实现 Thread 间传值

这篇具有很好参考价值的文章主要介绍了C# 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();
        }
    }

C# AsyncLocal 是如何实现 Thread 间传值

从截图看 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#是怎么流转的图吧:

C# AsyncLocal 是如何实现 Thread 间传值

有了这张图之后接下来的问题就是验证了,首先看一下 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=00007ff8b4a529d0
 r8=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:   no
                                                                                                            Lock  
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    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#层面玩法,希望能给后来者解惑吧!文章来源地址https://www.toymoban.com/news/detail-824528.html


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

C# AsyncLocal 是如何实现 Thread 间传值

到了这里,关于C# AsyncLocal 是如何实现 Thread 间传值的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 小程序父子组件间传值(微信/支付宝/钉钉)

    以传递一个简单的count值为例,实现父子组件的双向绑定,无论是点击父组件按钮还是子组件按钮,count值都自增 父传子 简单的将父组件在data中定义的属性,赋给子组件定义在properties中的属性即可 通过在父组件中设置一个按钮,使count自增,子组件的count属性也会随之自增

    2024年02月10日
    浏览(80)
  • vue组件间传值的六种方法

    父组件代码: 子组件代码: 页面显示: 子组件代码: 父组件代码: 页面显示: 父子组件传值原理:父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息,如下图所示: ref:如果在普通的 DOM 元素

    2024年02月15日
    浏览(50)
  • Flutter框架中,组件间传值的基本方式

    1.构造函数传值 在创建widget时,可以通过构造函数将参数传递给子widget。例如: 2.回调函数传值 父widget可以在创建子widget时,传递一个回调函数,当子widget需要把值传给父widget时,就调用这个回调函数。例如: 3.InheritedWidget传值 InheritedWidget是Flutter框架提供的一种特殊的wid

    2024年02月09日
    浏览(41)
  • 清除ExecutionContext,阻止 AsyncLocal 在异步流、Thread中传递

    前言: 自从使用了 AsyncLocal 后,就发现 AsyncLocal 变量像个臭虫一样,在有 AsyncLocal 变量的线程中启动的 Task 、或者 Thread 都会附带 AsyncLocal 变量 。 在项目使用 AsyncLocal 实现了全局、局部 工作单元 ,但是就无法在后续作业中开启多个线程了(需求就是要开启多个线程,俺

    2024年02月05日
    浏览(33)
  • c# picturebox实现label的功能,显示文字、背景色

    当然,你可以在C#中使用PictureBox控件来实现类似Label的显示文字功能。这并不是PictureBox的典型用途,但是可以通过绘图来实现。以下是一个简单的例子,展示了如何在PictureBox中绘制文字: 在这个示例中,我在PictureBox的Paint事件中添加了代码来绘制文字。你可以更改文字内容

    2024年02月14日
    浏览(37)
  • Unity3d C#利用Editor编辑器拓展实现配置UI背景样式一键设置UI背景样式功能(含源码)

    在开发UI滚动列表的时候,经常会有每项的背景图不统一的情况,会间隔重复的情况居多。这种情况下,手动去设置间隔一行的背景图或者颜色是比较麻烦的。在此背景下,笔者尝试写个小工具,在搭建UI时配置一下循环背景的样式,可以通过一键点击后设置UI背景的样式,省

    2024年02月03日
    浏览(52)
  • Qt如何实现动态背景-视频背景

    需求:加载视频作为视频背景,在上层可以进行图片的动画化,或是进行其他操作。 几种方法: 1、直接将视频弄成一个QDialog, 然后再上层在弄一个QDialog,背景透明即可。但遇到一个问题,QDialog没办法局部透明。 2、使用QOpenGLWidget: 这个方案没有使用。 3、使用QGraphicsVide

    2024年02月04日
    浏览(39)
  • C#基础--线程Thread和线程池ThreadPool

    1. 开启一个线程 ThreadStart 是属于System.Threading 中的一个内置委托 ParameterizedThreadStart 是属于System.Threading 中的一个内置委托 2. 暂停/恢复线程 无法实时的去 “暂停 ” 或者 “恢复” 线程,因为线程是由系统调度执行的,而且中间存在一个延时现象,不可能直接帮你执行 3. 终结

    2024年02月16日
    浏览(35)
  • C# .Net学习笔记—— 异步和多线程(Thread)

     注意:现在不建议使用 thread.Suspend(); //线程挂起                                  和 thread.Resum();   //唤醒线程 thread.Abort(); //销毁,方法是抛异常,也不建议使用 1、Join 线程等待 2、thread.ThreadState; //线程状态 3、thread.IsBackground; (i)默认是前台线程,启动之后一定要

    2024年02月02日
    浏览(41)
  • CSS中如何实现背景图片的平铺和定位?

    前端入门之旅:探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发者,这里都将为你提供一

    2024年02月10日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包