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

为什么用 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();}}

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

文章插图
图片
从代码中可以看到,确实如朋友所说,一个是num=10,一个是num=0 ,那为什么会出现这样的情况呢?
2. 从汇编上寻找答案作为C#程序员,真的需要掌握一点汇编,往往就能找到问题的突破口,先看一下thread1 中的 var j = num;所对应的汇编代码,参考如下:
D:codeMyApplicationConsoleApp7Program.cs @ 27:08893737 b9a0dd6808movecx,868DDA0h0889373c ba04000000movedx,408893741 e84a234e71callcoreclr!JIT_GetSharedNonGCThreadStaticBase (79d75a90)08893746 8b4814movecx,dword ptr [eax+14h]08893749 894df8movdword ptr [ebp-8],ecx从汇编上可以看到 , 这个 num=10 是来自于 eax+14h 的地址上,而 eax 是 JIT_GetSharedNonGCThreadStaticBase 函数的返回值 , 言外之意核心逻辑是在此方法里,可以到 coreclr 中找一下这段代码,简化后如下:
HCIMPL2(void*, JIT_GetSharedNonGCThreadStaticBase, DomainLocalModule *pDomainLocalModule, DWORD dwClassDomainID){FCALL_CONTRACT;// Get the ModuleIndexModuleIndex index = pDomainLocalModule->GetModuleIndex();// Get the relevant ThreadLocalModuleThreadLocalModule * 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 returnif (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 MethodTableMethodTable * pMT = pDomainLocalModule->GetMethodTableFromClassDomainID(dwClassDomainID);return HCCALL1(JIT_GetNonGCThreadStaticBase_Helper, pMT);}这段代码非常有意思 , 已经把 ThreadStatic 玩法的骨架图给绘制出来了,大概意思是每个线程都有一个 ThreadLocalBlock 结构体,这个结构体下有一个 ThreadLocalModule 的字典,key 为 ModuleIndex,value 为 ThreadLocalModule,画个简图如下:
C# 线程本地存储为什么线程间值不一样

文章插图
图片
从图中可以看到 num 是放在 ThreadLocalModule 中的,具体的说就是此结构的 m_pDataBlob 数组中 , 可以用 windbg 验证下 。
0:008> reax=03077810 ebx=08baf978 ecx=79d75c10 edx=03110568 esi=053faa18 edi=053fa9b8eip=08893746 esp=08baf8d8 ebp=08baf908 iopl=0nv up ei pl zr na pe nccs=0023ss=002bds=002bes=002bfs=0053gs=002befl=00000246ConsoleApp7!ConsoleApp7.Program.<>c.<Test>b__2_0+0x46:08893746 8b4814movecx,dword ptr [eax+14h] ds:002b:03077824=0000000a0: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 L1030778240000000a有了这些前置知识后,接下来就简单了 , 如果当前的 ThreadLocalModule 不存在就会调用 JIT_GetNonGCThreadStaticBase_Helper 函数在 m_pTLMTable 字段中添加一项,接下来观察下这个函数代码 , 简化如下:
HCIMPL1(void*, JIT_GetNonGCThreadStaticBase_Helper, MethodTable * pMT){// Get the TLMThreadLocalModule * pThreadLocalModule = ThreadStatics::GetTLM(pMT);// Check if the class constructor needs to be runpThreadLocalModule->CheckRunClassInitThrowing(pMT);// Lookup the non-GC statics base pointerbase = (void*) pMT->GetNonGCThreadStaticsBasePointer();return base;}PTR_ThreadLocalModule ThreadStatics::GetTLM(ModuleIndex index, Module * pModule) //static{// Get the TLM if it already existsPTR_ThreadLocalModule pThreadLocalModule = ThreadStatics::GetTLMIfExists(index);// If the TLM does not exist, create it nowif (pThreadLocalModule == NULL){// Allocate and initialize the TLM, and add it to the TLB's tablepThreadLocalModule = AllocateAndInitTLM(index, pThreadLocalBlock, pModule);}return pThreadLocalModule;}


推荐阅读