您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > C/C++/C#

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

时间:2024-01-26 13:36:45  来源:微信公众号  作者:一线码农聊技术
为什么用 ThreadStatic 标记的字段,只有第一个线程拿到了初始值,其他线程都是默认值,让我能不能帮他解答一下,尼玛,我也不是神仙什么都懂,既然问了,那我试着帮他解答一下,也给后面类似疑问的朋友解个惑吧。

一:背景

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



Tags:C#   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除。
▌相关推荐
C# 线程本地存储为什么线程间值不一样
为什么用 ThreadStatic 标记的字段,只有第一个线程拿到了初始值,其他线程都是默认值,让我能不能帮他解答一下,尼玛,我也不是神仙什么都懂,既然问了,那我试着帮他解答一下,也给后面类...【详细内容】
2024-01-26  Tags: C#  点击:(2)  评论:(0)  加入收藏
C# 登顶!超越Java或非空想
整理丨诺亚出品 | 51CTO技术栈(微信号:blog51cto)近日,TIOBE编程社区公布年度编程语言,此次摘得这一桂冠的是C#。这也是C#在TIOBE二十多年评选历史中首次赢得这一年度大奖。C#虽...【详细内容】
2024-01-15  Tags: C#  点击:(13)  评论:(0)  加入收藏
C#进程间消息传递
C#是一种流行的编程语言,它可以用于开发Windows应用程序。在开发Windows应用程序时,有时需要进行进程间通信,以实现不同进程之间的数据传递和交互。C#提供了多种方式来进行进程...【详细内容】
2024-01-01  Tags: C#  点击:(24)  评论:(0)  加入收藏
C#参数传递
前几天一个学员在学习C#与参数传递交互时,也不知道参数传递可以用来做什么 。下面我们就详细讲讲C# 和参数传递交互的相关知识。C#是一种面向对象的编程语言,支持多种参数传...【详细内容】
2023-11-11  Tags: C#  点击:(125)  评论:(0)  加入收藏
C#与高级控件
前几天一个学员在学习C#与高级控件交互时,也不知道高级控件可以用来做什么 。下面我们就详细讲讲C# 和高级控件交互的相关知识。C#是一种功能丰富的面向对象编程语言,它包含...【详细内容】
2023-11-10  Tags: C#  点击:(165)  评论:(0)  加入收藏
如何在C#客户端程序中无缝集成Python算法
背景介绍在软件开发领域,C#是一种广泛应用的面向对象编程语言,具有强大的类型系统和丰富的库支持。它常被用于开发Windows桌面应用程序、Web应用程序和服务端应用程序等。然而...【详细内容】
2023-11-03  Tags: C#  点击:(175)  评论:(0)  加入收藏
C#开发三个重要的内存区域:托管堆内存、非托管堆内存和栈内存
对内存的管理和操作大部分都是由 .NET 运行时处理的。开发者无需过多关注内存管理的细节,因为托管堆内存的垃圾回收机制可以自动处理对象的分配和释放。然而,在特定情况下,如与...【详细内容】
2023-11-01  Tags: C#  点击:(185)  评论:(0)  加入收藏
C#中LINQ的使用知多少?LINQ常功能整理,源代码解析
LINQ(Language-Integrated Query)是C#语言中的一个强大的查询技术,它提供了一种统一的查询语法,可以用于查询和操作各种数据源,包括集合、数据库、XML等。下面详细描述了LINQ的20...【详细内容】
2023-10-16  Tags: C#  点击:(194)  评论:(0)  加入收藏
我们一起聊聊C#堆排序算法
前言堆排序是一种高效的排序算法,基于二叉堆数据结构实现。它具有稳定性、时间复杂度为O(nlogn)和空间复杂度为O(1)的特点。堆排序实现原理 构建最大堆:将待排序数组构建成一...【详细内容】
2023-10-10  Tags: C#  点击:(188)  评论:(0)  加入收藏
如何通过 C# 比较两幅图片的相似度?
咨询区 Byyo我在用 C# 实现一个可以查找重复图片的小工具,我目前是给每一个图片做一个 md5 码,然后通过 md5 值来判断图片是否相同。但现实情况要复杂的多,比如: 图片被旋转...【详细内容】
2023-09-28  Tags: C#  点击:(202)  评论:(0)  加入收藏
▌简易百科推荐
C++代码优化攻略
今天我们将深入探讨C++性能优化的世界。在当今软件开发的浪潮中,高性能的代码是必不可少的。无论是开发桌面应用、移动应用,还是嵌入式系统,性能都是关键。1. 选择合适的数据结...【详细内容】
2024-01-26  AI让生活更美好  微信公众号  Tags:C++   点击:(2)  评论:(0)  加入收藏
C# 线程本地存储为什么线程间值不一样
为什么用 ThreadStatic 标记的字段,只有第一个线程拿到了初始值,其他线程都是默认值,让我能不能帮他解答一下,尼玛,我也不是神仙什么都懂,既然问了,那我试着帮他解答一下,也给后面类...【详细内容】
2024-01-26  一线码农聊技术  微信公众号  Tags:C#   点击:(2)  评论:(0)  加入收藏
C++质数检测器的设计与实现​
质数,作为数学中的一个基本概念,一直以其独特的性质吸引着众多研究者和爱好者。质数是指大于1的自然数中,除了1和它本身以外不再有其他因数的数。在实际应用中,质数检测也扮演着...【详细内容】
2024-01-15  鲨鱼编程  微信公众号  Tags:C++   点击:(19)  评论:(0)  加入收藏
C# 登顶!超越Java或非空想
整理丨诺亚出品 | 51CTO技术栈(微信号:blog51cto)近日,TIOBE编程社区公布年度编程语言,此次摘得这一桂冠的是C#。这也是C#在TIOBE二十多年评选历史中首次赢得这一年度大奖。C#虽...【详细内容】
2024-01-15    51CTO  Tags:C#   点击:(13)  评论:(0)  加入收藏
指针变量在C/C++中的内存占用
在编程领域,尤其是C和C++这类底层语言中,指针是一个核心概念,它允许程序直接操作内存地址。然而,关于指针本身在内存中占用的空间大小,却常常让初学者感到困惑。本文将深入探讨这...【详细内容】
2024-01-09  鲨鱼编程  微信公众号  Tags:指针变量   点击:(15)  评论:(0)  加入收藏
C++的面向对象编程:深入解析与理解
当我们谈论C++时,面向对象编程(OOP)是一个无法回避的话题。那么,C++的面向对象究竟是什么?为什么它如此重要?本文将从基本概念到实际应用,为您详细解析C++中的面向对象编程。一、面...【详细内容】
2024-01-03  鲨鱼编程  微信公众号  Tags:C++   点击:(22)  评论:(0)  加入收藏
C#进程间消息传递
C#是一种流行的编程语言,它可以用于开发Windows应用程序。在开发Windows应用程序时,有时需要进行进程间通信,以实现不同进程之间的数据传递和交互。C#提供了多种方式来进行进程...【详细内容】
2024-01-01  华山自控编程    Tags:C#   点击:(24)  评论:(0)  加入收藏
C语言中的volatile:变量的易变性和内存访问的优化
概念:在C语言中,volatile是一个关键字,用于告诉编译器变量的值是易变的,可能会在意料之外的情况下发生改变,从而防止编译器对该变量的优化和缓存。volatile关键字用于修饰那些可...【详细内容】
2023-12-31  极客代码  今日头条  Tags:C语言   点击:(6)  评论:(0)  加入收藏
C语言中的静态变量解析
一、引言在C语言中,变量的存储类别决定了变量在程序中的生命周期和可见性。静态变量是其中一种具有特殊属性的变量,它们在程序的执行过程中具有持久的生命周期,并且仅在其定义...【详细内容】
2023-12-29  鲨鱼编程  微信公众号  Tags:C语言   点击:(36)  评论:(0)  加入收藏
有什么好用的C/C++源代码混淆工具?
开始使用ipaguard前言iOS加固保护是直接针对ios ipa二进制文件的保护技术,可以对iOS APP中的可执行文件进行深度混淆、加密。使用任何工具都无法逆向、破解还原源文件。对APP...【详细内容】
2023-12-29  慕ie    Tags:C++   点击:(32)  评论:(0)  加入收藏
站内最新
站内热门
站内头条