网站cms是什么意思,小说网站开发的看书软件,东莞市手机网站建设公司,天天联盟广告网站如何做一#xff1a;背景 1. 讲故事
这个问题的由来是在.NET高级调试训练营第十期分享ThreadStatic底层玩法的时候#xff0c;有朋友提出了AsyncLocal是如何实现的#xff0c;虽然做了口头上的表述#xff0c;但总还是会不具体#xff0c;所以觉得有必要用文字图表的方式来系统…
一背景 1. 讲故事
这个问题的由来是在.NET高级调试训练营第十期分享ThreadStatic底层玩法的时候有朋友提出了AsyncLocal是如何实现的虽然做了口头上的表述但总还是会不具体所以觉得有必要用文字图表的方式来系统的说一下这个问题。 二AsyncLocal 线程间传值 1. 线程间传值途径
在 C# 编程中实现多线程以及线程切换的方式大概如下三种 Thread Task awaitasync
这三种场景下的线程间传值有各自的实现方式由于篇幅限制先从 Thread 开始聊吧。本质上来说 AsyncLocal 是一个纯托管的C#玩法和 coreclrWindows 没有任何关系。 2. Thread 小例子
为了方便讲述先来一个例子看下如何在新Thread线程中提取 _asyncLocal 中的值参考代码如下
internal class Program{static AsyncLocalint _asyncLocal new AsyncLocalint();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();}}从截图看 tid7 线程果然拿到了 主线程设置的 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:
00007ff90fe8e8c0 4c8bd1 mov r10,rcx
0:000 r
rax00007ff8b4a529d0 rbx0000000000000000 rcx0000008471b7df28
rdx00000000001fffff rsi0000027f2ca25b01 rdi0000027f2ca25b60
rip00007ff90fe8e8c0 rsp0000008471b7de68 rbp00007ff8b4a529d0r80000000000000000 r9ffffffffffffffff r100000027f2c8a0000
r110000008471b7de40 r120000008471b7e890 r130000008471b7e4f8
r14ffffffffffffffff r150000000000010000
iopl0 nv up ei pl nz na po nc
cs0033 ss002b ds002b es002b fs0053 gs002b efl00000206
ntdll!NtCreateThreadEx:
00007ff90fe8e8c0 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 方法的第二个参数即 rdi0000027f2ca25b60 就是我们的托管线程如果你不相信的话可以再用 windbg 找到它的托管线程信息输出如下 0:000 dt coreclr!Thread 0000027F2CA25B60 -y m_ExposedObject0x1c8 m_ExposedObject : 0x0000027f2c8f11d0 OBJECTHANDLE__0:000 !do poi(0x0000027f2c8f11d0)
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 ...hreadStartHelper 0 instance 0000027f2e5db3b0 _startHelper
...有些朋友可能要说你现在的 _executionContext 字段是保留在 _startHelper 类里并没有赋值到Thread._executionContext字段呀那这一块在哪里实现的呢从上图可以看到其实是在新线程的执行函数上在托管函数执行之前会将 _startHelper._executionContext 赋值给 Thread._executionContext , 让 windbg 继续执行输出如下 0:009 k# Child-SP RetAddr Call Site
00 00000084728ff778 00007ff8b4c23d19 KERNELBASE!wil::details::DebugBreak0x2
01 00000084728ff780 00007ff8b43ba7ea coreclr!DebugDebugger::Break0x149 [D:\a\_work\1\s\src\coreclr\vm\debugdebugger.cpp 148]
02 00000084728ff900 00007ff854ff56e3 System_Private_CoreLib!System.Diagnostics.Debugger.Break0xa [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs 18]
03 00000084728ff930 00007ff8b42b4259 ConsoleApp9!ConsoleApp9.Program.c.Mainb__1_00x113
04 00000084728ff9c0 00007ff8b42bddd9 System_Private_CoreLib!System.Threading.Thread.StartHelper.Callback0x39 [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs 42]
05 00000084728ffa00 00007ff8b42b2f4a System_Private_CoreLib!System.Threading.ExecutionContext.RunInternal0x69 [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs 183]
06 00000084728ffa70 00007ff8b4b7ba53 System_Private_CoreLib!System.Threading.Thread.StartCallback0x8a [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs 105]
07 00000084728ffab0 00007ff8b4a763dc coreclr!CallDescrWorkerInternal0x83
08 00000084728ffaf0 00007ff8b4b5e713 coreclr!DispatchCallSimple0x80 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp 220]
09 00000084728ffb80 00007ff8b4a52d25 coreclr!ThreadNative::KickOffThread_Worker0x63 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp 158]
...
0d (Inline Function) ---------------- coreclr!ManagedThreadBase_FullTransition0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp 7569]
0e (Inline Function) ---------------- coreclr!ManagedThreadBase::KickOff0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp 7604]
0f 00000084728ffd60 00007ff90e777614 coreclr!ThreadNative::KickOffThread0x79 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp 230]
10 00000084728ffdc0 00007ff90fe426a1 KERNEL32!BaseThreadInitThunk0x14
11 00000084728ffdf0 0000000000000000 ntdll!RtlUserThreadStart0x21
...在上面的回调函数中看的非常清楚在执行托管函数 Mainb__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 中在 Mainb__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
);