翼度科技»论坛 编程开发 .net 查看内容

C# 线程本地存储 为什么线程间值不一样

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
一:背景

1. 讲故事

有朋友在微信里面问我,为什么用 ThreadStatic 标记的字段,只有第一个线程拿到了初始值,其他线程都是默认值,让我能不能帮他解答一下,尼玛,我也不是神仙什么都懂,既然问了,那我试着帮他解答一下,也给后面类似疑问的朋友解个惑吧。
二:为什么值不一样

1. 问题复现

为了方便讲述,定义一个 ThreadStatic 的变量,然后用多个线程去访问,参考代码如下:
  1. internal class Program
  2. {
  3.     [ThreadStatic]
  4.     public static int num = 10;
  5.     static void Main(string[] args)
  6.     {
  7.         Test();
  8.         Console.ReadLine();
  9.     }
  10.     /// <summary>
  11.     /// 1. 特性方式
  12.     /// </summary>
  13.     static void Test()
  14.     {
  15.         var t1 = new Thread(() =>
  16.         {
  17.             Debugger.Break();
  18.             var j = num;
  19.             Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId}, num={j}");
  20.         });
  21.         t1.Start();
  22.         t1.Join();
  23.         var t2 = new Thread(() =>
  24.         {
  25.             Debugger.Break();
  26.             var j = num;
  27.             Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId}, num={j}");
  28.         });
  29.         t2.Start();
  30.     }
  31. }
复制代码

从代码中可以看到,确实如朋友所说,一个是num=10,一个是num=0 ,那为什么会出现这样的情况呢?
2. 从汇编上寻找答案

作为C#程序员,真的需要掌握一点汇编,往往就能找到问题的突破口,先看一下thread1 中的 var j = num;所对应的汇编代码,参考如下:
  1. D:\code\MyApplication\ConsoleApp7\Program.cs @ 27:
  2. 08893737 b9a0dd6808      mov     ecx,868DDA0h
  3. 0889373c ba04000000      mov     edx,4
  4. 08893741 e84a234e71      call    coreclr!JIT_GetSharedNonGCThreadStaticBase (79d75a90)
  5. 08893746 8b4814          mov     ecx,dword ptr [eax+14h]
  6. 08893749 894df8          mov     dword ptr [ebp-8],ecx
复制代码
从汇编上可以看到,这个 num=10 是来自于 eax+14h 的地址上,而 eax 是 JIT_GetSharedNonGCThreadStaticBase 函数的返回值,言外之意核心逻辑是在此方法里,可以到 coreclr 中找一下这段代码,简化后如下:
  1. HCIMPL2(void*, JIT_GetSharedNonGCThreadStaticBase, DomainLocalModule *pDomainLocalModule, DWORD dwClassDomainID)
  2. {
  3.     FCALL_CONTRACT;
  4.     // Get the ModuleIndex
  5.     ModuleIndex index = pDomainLocalModule->GetModuleIndex();
  6.     // Get the relevant ThreadLocalModule
  7.     ThreadLocalModule * pThreadLocalModule = ThreadStatics::GetTLMIfExists(index);
  8.     // If the TLM has been allocated and the class has been marked as initialized,
  9.     // get the pointer to the non-GC statics base and return
  10.     if (pThreadLocalModule != NULL && pThreadLocalModule->IsPrecomputedClassInitialized(dwClassDomainID))
  11.         return (void*)pThreadLocalModule->GetPrecomputedNonGCStaticsBasePointer();
  12.     // If the TLM was not allocated or if the class was not marked as initialized
  13.     // then we have to go through the slow path
  14.     // Obtain the MethodTable
  15.     MethodTable * pMT = pDomainLocalModule->GetMethodTableFromClassDomainID(dwClassDomainID);
  16.     return HCCALL1(JIT_GetNonGCThreadStaticBase_Helper, pMT);
  17. }
复制代码
这段代码非常有意思,已经把 ThreadStatic 玩法的骨架图给绘制出来了,大概意思是每个线程都有一个 ThreadLocalBlock 结构体,这个结构体下有一个 ThreadLocalModule 的字典,key 为 ModuleIndex, value 为 ThreadLocalModule,画个简图如下:

从图中可以看到 num 是放在 ThreadLocalModule 中的,具体的说就是此结构的 m_pDataBlob 数组中,可以用 windbg 验证下。
  1. 0:008> r
  2. eax=03077810 ebx=08baf978 ecx=79d75c10 edx=03110568 esi=053faa18 edi=053fa9b8
  3. eip=08893746 esp=08baf8d8 ebp=08baf908 iopl=0         nv up ei pl zr na pe nc
  4. cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
  5. ConsoleApp7!ConsoleApp7.Program.<>c.<Test>b__2_0+0x46:
  6. 08893746 8b4814          mov     ecx,dword ptr [eax+14h] ds:002b:03077824=0000000a
  7. 0:008> dt coreclr!ThreadLocalModule 03077810
  8.    +0x000 m_pDynamicClassTable : (null)
  9.    +0x004 m_aDynamicEntries : 0
  10.    +0x008 m_pGCStatics     : (null)
  11.    +0x00c m_pDataBlob      : [0]  ""
  12. 0:008> dp 03077810+0x14 L1
  13. 03077824  0000000a
复制代码
有了这些前置知识后,接下来就简单了,如果当前的 ThreadLocalModule 不存在就会调用 JIT_GetNonGCThreadStaticBase_Helper 函数在 m_pTLMTable 字段中添加一项,接下来观察下这个函数代码,简化如下:
  1. HCIMPL1(void*, JIT_GetNonGCThreadStaticBase_Helper, MethodTable * pMT)
  2. {
  3.     // Get the TLM
  4.     ThreadLocalModule * pThreadLocalModule = ThreadStatics::GetTLM(pMT);
  5.     // Check if the class constructor needs to be run
  6.     pThreadLocalModule->CheckRunClassInitThrowing(pMT);
  7.     // Lookup the non-GC statics base pointer
  8.     base = (void*) pMT->GetNonGCThreadStaticsBasePointer();
  9.     return base;
  10. }
  11. PTR_ThreadLocalModule ThreadStatics::GetTLM(ModuleIndex index, Module * pModule) //static
  12. {
  13.     // Get the TLM if it already exists
  14.     PTR_ThreadLocalModule pThreadLocalModule = ThreadStatics::GetTLMIfExists(index);
  15.     // If the TLM does not exist, create it now
  16.     if (pThreadLocalModule == NULL)
  17.     {
  18.         // Allocate and initialize the TLM, and add it to the TLB's table
  19.         pThreadLocalModule = AllocateAndInitTLM(index, pThreadLocalBlock, pModule);
  20.     }
  21.     return pThreadLocalModule;
  22. }
复制代码
上面这段代码的步骤很清楚。

  • 创建 ThreadLocalModule
  • 初始化 MethodTable 类型的字段 pMT
这个 pMT 非常重要,训练营里的朋友都知道 MethodTable 是 C# 的 class 承载,言外之意就是判断下这个 class 有没有被初始化,如果没有初始化那就调 静态构造函数,接下来的问题是 class 到底是哪一个类呢?
结合刚才汇编中的 mov edx,4 以及源码发现是取 IL 元数据中的 Program,参考代码及截图如下:
  1.     FORCEINLINE MethodTable * GetMethodTableFromClassDomainID(DWORD dwClassDomainID)
  2.     {
  3.         DWORD rid = (DWORD)(dwClassDomainID) + 1;
  4.         TypeHandle th = GetDomainFile()->GetModule()->LookupTypeDef(TokenFromRid(rid, mdtTypeDef));
  5.         MethodTable * pMT = th.AsMethodTable();
  6.         return pMT;
  7.     }
复制代码

也可以用 windbg 在 JIT_GetNonGCThreadStaticBase_Helper 方法的 return 处下一个断点,参考如下:
  1. 0:008> r ecx
  2. ecx=0564ef28
  3. 0:008> !dumpmt 0564ef28
  4. EEClass:             056d14d0
  5. Module:              0564db08
  6. Name:                ConsoleApp7.Program
  7. mdToken:             02000005
  8. File:                D:\code\MyApplication\ConsoleApp7\bin\x86\Debug\net6.0\ConsoleApp7.dll
  9. AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
  10. BaseSize:            0xc
  11. ComponentSize:       0x0
  12. DynamicStatics:      false
  13. ContainsPointers:    false
  14. Slots in VTable:     8
  15. Number of IFaces in IFaceMap: 0
复制代码
到这里就真相大白了,thread1 在执行时,用 CheckRunClassInitThrowing 方法发现 Program 没有被静态构造过,所以就执行了,即 num=10 ,当 thread2 执行时,发现已经被构造过了,所以就不再执行静态构造函数,所以就成了默认值 num=0。
3. 如何复验你的结论

刚才我说 thread1 做了一个是否执行静态构造的判断,其实这里我可以做个手脚,在 Main 之前先把 Program 静态函数给执行掉,按理说 thread1 和 thread2 此时都会是默认值 num=0,对不对,哈哈,试一试呗,简化代码如下:
  1.     internal class Program
  2.     {
  3.         [ThreadStatic]
  4.         public static int num = 10;
  5.         /// <summary>
  6.         /// 先于 main 执行
  7.         /// </summary>
  8.         static Program()
  9.         {
  10.         }
  11.         static void Main(string[] args)
  12.         {
  13.             Test();
  14.             Console.ReadLine();
  15.         }
  16.     }
复制代码

哈哈,此时都是 0 了,也就再次验证了我的结论。
三:总结

在 C# 开发中经常会有一些疑惑,如果不了解汇编,C++ ,相信你会陷入到很多的魔法使用中而苦于不能独自解惑的遗憾。

来源:https://www.cnblogs.com/huangxincheng/p/17982804
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具