一:背景
1. 讲故事
有朋友在微信里面问我,为什么用
二:为什么值不一样
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();
}
}

从代码中可以看到,确实如朋友所说,一个是
2. 从汇编上寻找答案
作为C#程序员,真的需要掌握一点汇编,往往就能找到问题的突破口,先看一下thread1 中的
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 是来自于
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);
}
这段代码非常有意思,已经把

从图中可以看到 num 是放在 ThreadLocalModule 中的,具体的说就是此结构的
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 有没有被初始化,如果没有初始化那就调
结合刚才汇编中的
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:codeMyApplicationConsoleApp7inx86Debug et6.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 没有被静态构造过,所以就执行了,即
3. 如何复验你的结论
刚才我说 thread1 做了一个是否执行静态构造的判断,其实这里我可以做个手脚,在 Main 之前先把 Program 静态函数给执行掉,按理说 thread1 和 thread2 此时都会是默认值
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++ ,相信你会陷入到很多的魔法使用中而苦于不能独自解惑的遗憾。