一:背景

1. 讲故事

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

二:为什么值不一样

1. 问题复现

为了方便讲述,定义一个 ThreadStatic 的变量,然后用多个线程去访问,参考代码如下:


internal class Program
{
[ThreadStatic]
public static int num = 10; static void Main(string[] args)
{
Test(); Console.ReadLine();
} /// <summary>
/// 1. 特性方式
/// </summary>
static void Test()
{
var t1 = new Thread(() =>
{
Debugger.Break();
var j = num;
Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId}, num={j}"); });
t1.Start();
t1.Join(); var t2 = new Thread(() =>
{
Debugger.Break();
var j = num;
Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId}, num={j}");
}); t2.Start();
}
}

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

2. 从汇编上寻找答案

作为C#程序员,真的需要掌握一点汇编,往往就能找到问题的突破口,先看一下thread1 中的 var j = num;所对应的汇编代码,参考如下:


D:\code\MyApplication\ConsoleApp7\Program.cs @ 27:
08893737 b9a0dd6808 mov ecx,868DDA0h
0889373c ba04000000 mov edx,4
08893741 e84a234e71 call coreclr!JIT_GetSharedNonGCThreadStaticBase (79d75a90)
08893746 8b4814 mov ecx,dword ptr [eax+14h]
08893749 894df8 mov dword ptr [ebp-8],ecx

从汇编上可以看到,这个 num=10 是来自于 eax+14h 的地址上,而 eax 是 JIT_GetSharedNonGCThreadStaticBase 函数的返回值,言外之意核心逻辑是在此方法里,可以到 coreclr 中找一下这段代码,简化后如下:


HCIMPL2(void*, JIT_GetSharedNonGCThreadStaticBase, DomainLocalModule *pDomainLocalModule, DWORD dwClassDomainID)
{
FCALL_CONTRACT; // Get the ModuleIndex
ModuleIndex index = pDomainLocalModule->GetModuleIndex(); // Get the relevant ThreadLocalModule
ThreadLocalModule * pThreadLocalModule = ThreadStatics::GetTLMIfExists(index); // If the TLM has been allocated and the class has been marked as initialized,
// get the pointer to the non-GC statics base and return
if (pThreadLocalModule != NULL && pThreadLocalModule->IsPrecomputedClassInitialized(dwClassDomainID))
return (void*)pThreadLocalModule->GetPrecomputedNonGCStaticsBasePointer(); // If the TLM was not allocated or if the class was not marked as initialized
// then we have to go through the slow path // Obtain the MethodTable
MethodTable * pMT = pDomainLocalModule->GetMethodTableFromClassDomainID(dwClassDomainID); return HCCALL1(JIT_GetNonGCThreadStaticBase_Helper, pMT);
}

这段代码非常有意思,已经把 ThreadStatic 玩法的骨架图给绘制出来了,大概意思是每个线程都有一个 ThreadLocalBlock 结构体,这个结构体下有一个 ThreadLocalModule 的字典,key 为 ModuleIndex, value 为 ThreadLocalModule,画个简图如下:

从图中可以看到 num 是放在 ThreadLocalModule 中的,具体的说就是此结构的 m_pDataBlob 数组中,可以用 windbg 验证下。


0:008> r
eax=03077810 ebx=08baf978 ecx=79d75c10 edx=03110568 esi=053faa18 edi=053fa9b8
eip=08893746 esp=08baf8d8 ebp=08baf908 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ConsoleApp7!ConsoleApp7.Program.<>c.<Test>b__2_0+0x46:
08893746 8b4814 mov ecx,dword ptr [eax+14h] ds:002b:03077824=0000000a 0:008> dt coreclr!ThreadLocalModule 03077810
+0x000 m_pDynamicClassTable : (null)
+0x004 m_aDynamicEntries : 0
+0x008 m_pGCStatics : (null)
+0x00c m_pDataBlob : [0] "" 0:008> dp 03077810+0x14 L1
03077824 0000000a

有了这些前置知识后,接下来就简单了,如果当前的 ThreadLocalModule 不存在就会调用 JIT_GetNonGCThreadStaticBase_Helper 函数在 m_pTLMTable 字段中添加一项,接下来观察下这个函数代码,简化如下:


HCIMPL1(void*, JIT_GetNonGCThreadStaticBase_Helper, MethodTable * pMT)
{
// Get the TLM
ThreadLocalModule * pThreadLocalModule = ThreadStatics::GetTLM(pMT); // Check if the class constructor needs to be run
pThreadLocalModule->CheckRunClassInitThrowing(pMT); // Lookup the non-GC statics base pointer
base = (void*) pMT->GetNonGCThreadStaticsBasePointer(); return base;
} PTR_ThreadLocalModule ThreadStatics::GetTLM(ModuleIndex index, Module * pModule) //static
{
// Get the TLM if it already exists
PTR_ThreadLocalModule pThreadLocalModule = ThreadStatics::GetTLMIfExists(index); // If the TLM does not exist, create it now
if (pThreadLocalModule == NULL)
{
// Allocate and initialize the TLM, and add it to the TLB's table
pThreadLocalModule = AllocateAndInitTLM(index, pThreadLocalBlock, pModule);
} return pThreadLocalModule;
}

上面这段代码的步骤很清楚。

  • 创建 ThreadLocalModule

  • 初始化 MethodTable 类型的字段 pMT

这个 pMT 非常重要,训练营里的朋友都知道 MethodTable 是 C# 的 class 承载,言外之意就是判断下这个 class 有没有被初始化,如果没有初始化那就调 静态构造函数,接下来的问题是 class 到底是哪一个类呢?

结合刚才汇编中的 mov edx,4 以及源码发现是取 IL 元数据中的 Program,参考代码及截图如下:


FORCEINLINE MethodTable * GetMethodTableFromClassDomainID(DWORD dwClassDomainID)
{
DWORD rid = (DWORD)(dwClassDomainID) + 1;
TypeHandle th = GetDomainFile()->GetModule()->LookupTypeDef(TokenFromRid(rid, mdtTypeDef));
MethodTable * pMT = th.AsMethodTable();
return pMT;
}

也可以用 windbg 在 JIT_GetNonGCThreadStaticBase_Helper 方法的 return 处下一个断点,参考如下:


0:008> r ecx
ecx=0564ef28
0:008> !dumpmt 0564ef28
EEClass: 056d14d0
Module: 0564db08
Name: ConsoleApp7.Program
mdToken: 02000005
File: D:\code\MyApplication\ConsoleApp7\bin\x86\Debug\net6.0\ConsoleApp7.dll
AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
BaseSize: 0xc
ComponentSize: 0x0
DynamicStatics: false
ContainsPointers: false
Slots in VTable: 8
Number of IFaces in IFaceMap: 0

到这里就真相大白了,thread1 在执行时,用 CheckRunClassInitThrowing 方法发现 Program 没有被静态构造过,所以就执行了,即 num=10 ,当 thread2 执行时,发现已经被构造过了,所以就不再执行静态构造函数,所以就成了默认值 num=0

3. 如何复验你的结论

刚才我说 thread1 做了一个是否执行静态构造的判断,其实这里我可以做个手脚,在 Main 之前先把 Program 静态函数给执行掉,按理说 thread1 和 thread2 此时都会是默认值 num=0,对不对,哈哈,试一试呗,简化代码如下:


internal class Program
{
[ThreadStatic]
public static int num = 10; /// <summary>
/// 先于 main 执行
/// </summary>
static Program()
{
} static void Main(string[] args)
{
Test(); Console.ReadLine();
}
}

哈哈,此时都是 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. 一键打包,随时运行,Python3项目虚拟环境一键整合包的制作(Venv)

    之前我们介绍了如何使用嵌入式 Python3 环境给项目制作一键整合包,在使用嵌入式 Python 环境时,通常是作为另一个应用程序的一部分,而Python3虚拟环境是为了在开发过程中隔离项目所需的 ...

  2. 神经网络优化篇:详解梯度的数值逼近(Numerical approximation of gradients)

    在实施backprop时,有一个测试叫做梯度检验,它的作用是确保backprop正确实施.因为有时候,虽然写下了这些方程式,却不能100%确定,执行backprop的所有细节都是正确的.为了逐渐实现梯 ...

  3. MySQL|MySQL事物以及隔离级别

    MySQL 事务主要用于处理操作量大,复杂度高的数据.比如开单,需要添加给订单表增加记录,还需要增加订单的各种相关明细,操作复杂度高,这些操作语句需要构成一个事务.在 MySQL 命令行的默认设置下, ...

  4. linux内核initcall放置在各个section中函数执行流程

    前言 linux以及嵌入式一些代码,我们看到core_initcall.device_initcall等等需要链接器分配各个section,并且在启动该模块时候执行.下面我们详细追溯一下执行过程. 作 ...

  5. vue-admin-template动态菜单后台获取菜单

    vue-admin-template.vue-element-admin配置动态菜单,菜单数据从后台获取. 我在网上search了几个小时也没有找到想要的emm,翻官网也没有说明,只说明了路由覆盖.只 ...

  6. MySQL 基础(四)锁

    解决并发事务带来的问题 写-写情况 任意一种事务隔离级别都不允许 "脏写" 的发生,因为这样会使得数据混乱.所以,当多个未提交的事务相继对一条记录进行改动时,就需要使得这些事务串行 ...

  7. Spark Core快速入门

    Spark-core快速入门 一.简介 Apache spark是专门为大规模数据处理而设计的快速通用的计算模型,是一种类似于Mapreduce通用并行计算框架,与mapreduce不同的是,spar ...

  8. 消除视觉Transformer与卷积神经网络在小数据集上的差距

    摘要:本文通过多种操作构建混合模型,增强视觉Transformer捕捉空间相关性的能力和其进行通道多样性表征的能力,弥补了Transformer在小数据集上从头训练的精度与传统的卷积神经网络之间的差距 ...

  9. k8s源码Client-go中Reflector解析

    摘要:通过本文,可以了解Reflector通过ListWatcher从Kubernetes API中获取对象的流程,以及存储到store中,后续会对DeltaFIFO进行源码研读,通过结合inform ...

  10. 火山引擎DataLeap数据质量解决方案和最佳实践(三):最佳实践

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 最佳实践 前面介绍了DataLeap数据质量平台的一些实现方式,下面为大家介绍一些我们在数据量和资源这两个方面的最 ...