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

一:背景

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/342685.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Dockerfile中CMD命令的用法,你了解几个?

文章目录 1.问题描述2.解决方法3.问题原因分析4.CMD常见的用法4.1 执行应用程序4.2 使用 Shell 执行命令4.3 定义环境变量 5.最佳实践 1.问题描述 今天在进行业务调试时, 发现调试的java应用所在的容器无限重启&#xff0c;经过排查发现问题出在Dockerfile 文件中的CMD命令上&…

设计模式下

一、代理模式 一、概念 代理模式就是给一个对象提供一个代理&#xff0c;并由代理对象控制对原对象的引 用。它使得客户不能直接与真正的目标对象通信。代理对象是目标对象的代表&#xff0c; 其他需要与这个目标对象打交道的操作都是和这个代理对象在交涉。 例如&#xff1a;去…

安装 nvm

前言&#xff1a; nvm 即 node 版本管理工具 (node version manager)&#xff0c;好处是方便切换 node.js 版本。 通过将多个 node 版本安装在指定路径&#xff0c;然后通过 nvm 命令切换时&#xff0c;就会切换我们环境变量中 node 命令指定的实际执行的软件路径。 使用场景…

Selenium自动化测试之学会元素定位

这是我经常被问到的一个问题&#xff0c;也是我很讨厌回答的问题&#xff0c;因为要想回答这个问题需要知道上下文。什么样式的元素&#xff0c;有哪些属性&#xff0c;是否有表单嵌套&#xff0c;都尝试了哪些定位方法。。。而且没几个提问者能事先详细的说明这些。哪儿像提bu…

宠物互联网医院系统:数字化呵护你爱宠的新时代

宠物互联网医院系统正在为宠物主人提供一种前所未有的数字化健康护理体验。通过结合创新技术&#xff0c;这一系统旨在让宠物医疗变得更加便捷、智能和个性化。让我们深入探讨宠物互联网医院系统的技术核心&#xff0c;以及如何应用代码为你的爱宠提供最佳关怀。 1. 远程医疗…

操作系统-进程控制(如何实现进程控制 如何实现原子性 相关进程控制原语)

文章目录 什么是进程控制总览如何实现进程控制&#xff1f;如何实现原语的“原子性”&#xff1f;进程控制相关的原语创建原语撤销原语子进程与父进程阻塞与唤醒原语切换原语 小结 什么是进程控制 控制进程的状态变换 总览 如何实现进程控制&#xff1f; 原语实现 假设不是原…

离线直线度测量仪的三种类型

智能型离线直线度测径仪主要用于金属棒材、管材、陶瓷管材等产品在生产节拍时间充裕时的直径和直线度检测。测量方便&#xff0c;自动将检测数据显示于显示屏上。 离线直线度测量仪使用前&#xff0c;设置产品规格&#xff0c;在更换轧材规格时&#xff0c;在设置一次&#xff…

强化学习:MuJoCo机器人强化学习仿真入门(1)

声明&#xff1a;我们跳过mujoco环境的搭建&#xff0c;搭建环境不难&#xff0c;可自行百度 下面开始进入正题&#xff08;需要有一定的python基础与xml基础&#xff09;&#xff1a; 下面进入到建立机器人模型的部分&#xff1a; 需要先介绍URDF模型文件和导出MJCF格式 介绍完…

【MATLAB源码-第123期】基于matlab的SSK(空间位移键控)调制和QSSK(正交空间位移键控)调制误码率对比。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. SSK&#xff08;空间位移键控&#xff09;&#xff1a; - 基本原理&#xff1a;SSK是一种MIMO&#xff08;多输入多输出&#xff09;系统中的调制技术。它通过选择性地激活不同的发送天线来传输信息。在每个时间槽&…

第十三回 赤发鬼醉卧灵官殿 晁天王认义东溪村-FreeBSD 下编辑文件ee 用法

话说雷横绑了一个大汉&#xff0c;先押到晁保正庄上讨些点心吃。这东溪村的保正叫晁盖&#xff0c;人称他托塔天王。晁盖热情接待了雷横和士兵们&#xff0c;自己却趁着他们喝酒的时候尿遁去看那大汉。原来那大汉就是来找晁保正的&#xff0c;于是谎称他是晁盖的外甥&#xff0…

【K8S 云原生】K8S的包包管理器-helm

目录 一、helm概念 1、什么是helm 2、helm的概念&#xff1a; 二、实验部署&#xff1a; 1、安装helm&#xff1a; 2、对chart仓库的基本使用&#xff1a; 2.1、查看和更新chart仓库 2.2、安装chart 2.3、卸载chart&#xff1a; 3、helm自定义模版&#xff1a; 3.1、…

【Linux】【实战系列】10 分钟掌握日常开发中 Linux 网络处理相关命令

文章目录 lsofnetstatpingnslookupsshssh-keygenscpsftp 网络工具 curl网络工具 wget最后个人简介 hello&#xff0c;大家好&#xff0c;我是 Lorin&#xff0c;上一期和大家分享一期日常开发中常用的 Linux 文件和文本命令实战教学&#xff0c;这一期给大家带来常用的网络处理…

k8s部署Ingress

前提&#xff1a;有自备的域名&#xff0c;没有域名可用主机配置的域名解析代替测试 #主机配置的域名 vi /etc/hosts #追加以下内容 127.0.0.1 kuboard1、部署Ingress vi deploy.yaml #输入以下内容apiVersion: v1 kind: Namespace metadata:labels:app.kubernetes.io/instan…

git使用指南——以gitlab为例

注册gitlab 自行注册 新建项目 选择新建一个空白的项目 上传项目 clone项目地址到本地 执行完之后&#xff0c;会在目录下生成如下内容&#xff1a;进入里面&#xff0c;选择.git&#xff0c;要上传的内容&#xff08;资料或代码复制到该目录下&#xff09;&#xff1a;…

华夏基金“冰火两重天”:产品增量不增值,靠什么赢得用户?

近日&#xff0c;华夏基金发布关于华夏野村日经225交易型开放式指数证券投资基金&#xff08;QDII&#xff09;&#xff08;下称“华夏野村日经ETF”&#xff09;二级市场交易价格溢价风险提示及临时停牌公告。 公告内容显示&#xff0c;华夏野村日经ETF二级市场交易价格明显高…

Prometheus 架构全面解析

在本指南中&#xff0c;我们将详细介绍 Prometheus 架构。 Prometheus 是一个用 Golang 编写的开源监控和告警系统&#xff0c;能够收集和处理来自各种目标的指标。您还可以查询、查看、分析指标&#xff0c;并根据阈值收到警报。 此外&#xff0c;在当今世界&#xff0c;可观…

如何使用宝塔面板搭建Discuz论坛服务并结合内网穿透远程访问

文章目录 前言1.安装基础环境2.一键部署Discuz3.安装cpolar工具4.配置域名访问Discuz5.固定域名公网地址6.配置Discuz论坛 前言 Crossday Discuz! Board&#xff08;以下简称 Discuz!&#xff09;是一套通用的社区论坛软件系统&#xff0c;用户可以在不需要任何编程的基础上&a…

架构篇18:单服务器高性能模式-PPC与TPC

文章目录 PPCpreforkTPCprethread小结高性能是每个程序员的追求,无论我们是做一个系统还是写一行代码,都希望能够达到高性能的效果,而高性能又是最复杂的一环,磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响系统达到高性能,一行不恰当的 deb…

【强化学习】QAC、A2C、A3C学习笔记

强化学习算法&#xff1a;QAC vs A2C vs A3C 引言 经典的REINFORCE算法为我们提供了一种直接优化策略的方式&#xff0c;它通过梯度上升方法来寻找最优策略。然而&#xff0c;REINFORCE算法也有其局限性&#xff0c;采样效率低、高方差、收敛性差、难以处理高维离散空间。 为…

力扣hot100 环形链表 快慢指针 哈希 数学公式

Problem: 142. 环形链表 II 文章目录 思路Code 思路 &#x1f468;‍&#x1f3eb; 参考题解 Code ⏰ 时间复杂度: O ( n ) O(n) O(n) &#x1f30e; 空间复杂度: O ( 1 ) O(1) O(1) /** /*** Definition for singly-linked list.* class ListNode {* int val;* …