一:背景

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++ ,相信你会陷入到很多的魔法使用中而苦于不能独自解惑的遗憾。

C# 线程本地存储 为什么线程间值不一样的更多相关文章

  1. Atitit usrqbg1821 Tls 线程本地存储(ThreadLocal Storage 规范标准化草案解决方案ThreadStatic

    Atitit usrqbg1821 Tls 线程本地存储(ThreadLocal Storage 规范标准化草案解决方案ThreadStatic 1.1. ThreadLocal 设计模式1 1.2. ...

  2. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    原文链接地址:http://www.cppblog.com/Tim/archive/2012/07/04/181018.html 本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们 ...

  3. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们知道在一个进程中,所有线程是共享同一个地址空间的.所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线 ...

  4. ThreadLocal(线程本地存储)

    1. ThreadLocal,即线程本地变量或线程本地存储. threadlocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的 ...

  5. 线程本地存储(动态TLS和静态TLS)

    线程本地存储(TLS) 对于多线程应用程序,如果线程过于依赖全局变量和静态局部变量就会产生线程安全问题.也就是一个线程的使用全局变量可能会影响到其他也使用此全局变量的线程,有可能会造成一定的错误,这可 ...

  6. 线程本地存储 ThreadLocal

    线程本地存储 · 语雀 (yuque.com) 线程本地存储提供了线程内存储变量的能力,这些变量是线程私有的. 线程本地存储一般用在跨类.跨方法的传递一些值. 线程本地存储也是解决特定场景下线程安全问 ...

  7. 线程本地存储(Thread Local Storage, TLS)简单分析与使用

    在多线程编程中, 同一个变量, 如果要让多个线程共享访问, 那么这个变量可以使用关键字volatile进行声明; 那么如果一个变量不想使多个线程共享访问, 那么该怎么办呢? 呵呵, 这个办法就是TLS ...

  8. .NET:线程本地存储、调用上下文、逻辑调用上下文

    .NET:线程本地存储.调用上下文.逻辑调用上下文 目录 背景线程本地存储调用上下文逻辑调用上下文备注 背景返回目录 在多线程环境,如果需要将实例的生命周期控制在某个操作的执行期间,该如何设计?经典的 ...

  9. C# 线程本地存储 调用上下文 逻辑调用上下文

    线程本地存储 using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleAppTest ...

  10. Java线程本地存储ThreadLocal

    前言 ThreadLocal 是一种 无同步 的线程安全实现 体现了 Thread-Specific Storage 模式:即使只有一个入口,内部也会为每个线程分配特有的存储空间,线程间 没有共享资源 ...

随机推荐

  1. 【C#】【IO】【Threading】【实例】工作报表前的本地数据聚合操作

    <工作记录--Report> 报表前的数据获取操作是高重复性的,今天差不多完成了脚本,下述是代码: 1 // See https://aka.ms/new-console-template ...

  2. 小姐姐用动画图解Git命令,一看就懂!

    无论是开发.运维,还是测试,大家都知道Git在日常工作中的地位.所以,也是大家的必学.必备技能之一.之前公众号也发过很多git相关的文章: Git这些高级用法,喜欢就拿去用!一文速查Git常用命令,搞 ...

  3. k8s 标签-2

    目录 标签-2 node的角色 修改node节点的角色,将他的角色修改成他的主机名 标签的作用 Cordon,Drain以及污点 Cordon--告警警戒 Drain 驱逐演示 污点 污点的Cordo ...

  4. DevOps常用工具全家桶,实现高效运维和交付

    DevOps常用工具全家桶,实现高效运维和交付 1.DevOps发展 DevOps发展背景: 随着互联网技术的快速发展,软件开发和运维的挑战也日益增加.传统的软件开发和运维模式往往存在分离.效率低下. ...

  5. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 11.认识GAS & 创建自己的能力系统

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本篇文章对应Lecture 16 - Writing our own Gameplay Ability Syst ...

  6. 欢迎使用CSDN-markdown编辑器测试

    这里写自定义目录标题 欢迎使用Markdown编辑器 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 创建一 ...

  7. 云图说|数据仓库服务 GaussDB(DWS) 的“千里眼、顺风耳”—数据库智能运维

    摘要:数据库智能运维(DMS)是GaussDB(DWS) 为客户数据库快速.稳定运行提供保驾护航的能力,对业务数据库所使用磁盘.网络.OS指标数据,集群运行关键性能指标进行收集.监控.分析.通过综合收 ...

  8. update 没有索引导致业务崩了,老板骂了一个小时

    摘要:有天,一朋友在线上执行一条 update 语句修改数据库数据的时候,where 条件没有带上索引,导致业务直接崩了,被老板教训了一波. 本文分享自华为云社区<update 没有索引,会锁全 ...

  9. TypeScript里string和String,真不是仅仅是大小写的区别

    摘要:通常来说,string表示原生类型,而String表示对象. 本文分享自华为云社区<TypeScript里string和String的区别>,作者:gentle_zhou . 背景 ...

  10. 混合编程:如何用pybind11调用C++

    摘要:在实际开发过程中,免不了涉及到混合编程,比如,对于python这种脚本语言,性能还是有限的,在一些对性能要求高的情景下面,还是需要使用c/c++来完成. 本文分享自华为云社区<混合编程:如 ...