淺析C#線程本地存儲(chǔ)中為什么線程間值不一樣
一:背景
講故事
有朋友在微信里面問(wèn)我,為什么用 ThreadStatic
標(biāo)記的字段,只有第一個(gè)線程拿到了初始值,其他線程都是默認(rèn)值,讓我能不能幫他解答一下,尼瑪,我也不是神仙什么都懂,既然問(wèn)了,那我試著幫他解答一下,也給后面類(lèi)似疑問(wèn)的朋友解個(gè)惑吧。
二:為什么值不一樣
1. 問(wèn)題復(fù)現(xiàn)
為了方便講述,定義一個(gè) ThreadStatic 的變量,然后用多個(gè)線程去訪問(wèn),參考代碼如下:
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(); } }
從代碼中可以看到,確實(shí)如朋友所說(shuō),一個(gè)是num=10
,一個(gè)是num=0
,那為什么會(huì)出現(xiàn)這樣的情況呢?
2. 從匯編上尋找答案
作為C#程序員,真的需要掌握一點(diǎn)匯編,往往就能找到問(wèn)題的突破口,先看一下thread1 中的 var j = num;
所對(duì)應(yīng)的匯編代碼,參考如下:
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
從匯編上可以看到,這個(gè) num=10 是來(lái)自于 eax+14h
的地址上,而 eax 是 JIT_GetSharedNonGCThreadStaticBase 函數(shù)的返回值,言外之意核心邏輯是在此方法里,可以到 coreclr 中找一下這段代碼,簡(jiǎn)化后如下:
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); }
這段代碼非常有意思,已經(jīng)把 ThreadStatic
玩法的骨架圖給繪制出來(lái)了,大概意思是每個(gè)線程都有一個(gè) ThreadLocalBlock
結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體下有一個(gè) ThreadLocalModule
的字典,key 為 ModuleIndex, value 為 ThreadLocalModule,畫(huà)個(gè)簡(jiǎn)圖如下:
從圖中可以看到 num 是放在 ThreadLocalModule 中的,具體的說(shuō)就是此結(jié)構(gòu)的 m_pDataBlob
數(shù)組中,可以用 windbg 驗(yàn)證下。
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
有了這些前置知識(shí)后,接下來(lái)就簡(jiǎn)單了,如果當(dāng)前的 ThreadLocalModule 不存在就會(huì)調(diào)用 JIT_GetNonGCThreadStaticBase_Helper 函數(shù)在 m_pTLMTable 字段中添加一項(xiàng),接下來(lái)觀察下這個(gè)函數(shù)代碼,簡(jiǎn)化如下:
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; }
上面這段代碼的步驟很清楚。
- 創(chuàng)建 ThreadLocalModule
- 初始化 MethodTable 類(lèi)型的字段 pMT
這個(gè) pMT 非常重要,訓(xùn)練營(yíng)里的朋友都知道 MethodTable 是 C# 的 class 承載,言外之意就是判斷下這個(gè) class 有沒(méi)有被初始化,如果沒(méi)有初始化那就調(diào) 靜態(tài)構(gòu)造函數(shù)
,接下來(lái)的問(wèn)題是 class 到底是哪一個(gè)類(lèi)呢?
結(jié)合剛才匯編中的 mov edx,4
以及源碼發(fā)現(xiàn)是取 IL 元數(shù)據(jù)中的 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 處下一個(gè)斷點(diǎn),參考如下:
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 在執(zhí)行時(shí),用 CheckRunClassInitThrowing 方法發(fā)現(xiàn) Program 沒(méi)有被靜態(tài)構(gòu)造過(guò),所以就執(zhí)行了,即 num=10
,當(dāng) thread2 執(zhí)行時(shí),發(fā)現(xiàn)已經(jīng)被構(gòu)造過(guò)了,所以就不再執(zhí)行靜態(tài)構(gòu)造函數(shù),所以就成了默認(rèn)值 num=0
。
3. 如何復(fù)驗(yàn)?zāi)愕慕Y(jié)論
剛才我說(shuō) thread1 做了一個(gè)是否執(zhí)行靜態(tài)構(gòu)造的判斷,其實(shí)這里我可以做個(gè)手腳,在 Main 之前先把 Program 靜態(tài)函數(shù)給執(zhí)行掉,按理說(shuō) thread1 和 thread2 此時(shí)都會(huì)是默認(rèn)值 num=0
,對(duì)不對(duì),哈哈,試一試唄,簡(jiǎn)化代碼如下:
internal class Program { [ThreadStatic] public static int num = 10; /// <summary> /// 先于 main 執(zhí)行 /// </summary> static Program() { } static void Main(string[] args) { Test(); Console.ReadLine(); } }
哈哈,此時(shí)都是 0 了,也就再次驗(yàn)證了我的結(jié)論。
以上就是淺析C#線程本地存儲(chǔ)中為什么線程間值不一樣的詳細(xì)內(nèi)容,更多關(guān)于C#線程本地存儲(chǔ)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
.NET/C#實(shí)現(xiàn)識(shí)別用戶訪問(wèn)設(shè)備的方法
這篇文章主要介紹了.NET/C#實(shí)現(xiàn)識(shí)別用戶訪問(wèn)設(shè)備的方法,結(jié)合實(shí)例形式分析了C#識(shí)別用戶訪問(wèn)設(shè)備的操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02C#基礎(chǔ)知識(shí)系列八const和readonly關(guān)鍵字詳細(xì)介紹
這篇文章主要介紹了C#中的const和readonly關(guān)鍵字,有需要的朋友可以參考一下2014-01-01C#提取PDF表單數(shù)據(jù)的實(shí)現(xiàn)流程
PDF表單是一種常見(jiàn)的數(shù)據(jù)收集工具,廣泛應(yīng)用于調(diào)查問(wèn)卷、業(yè)務(wù)合同等場(chǎng)景,憑借出色的跨平臺(tái)兼容性和標(biāo)準(zhǔn)化特點(diǎn),PDF表單在各行各業(yè)中得到了廣泛應(yīng)用,本文將探討如何使用C# 實(shí)現(xiàn)自動(dòng)化PDF表單數(shù)據(jù)提取流程,需要的朋友可以參考下2025-01-01DevExpress之ChartControl實(shí)現(xiàn)柱狀圖演示實(shí)例
這篇文章主要介紹了DevExpress中ChartControl實(shí)現(xiàn)柱狀圖演示方法,實(shí)例展示了相關(guān)繪圖函數(shù)的具體用法,具有一定的實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10c#實(shí)現(xiàn)ini文件讀寫(xiě)類(lèi)分享
c#實(shí)現(xiàn)ini文件讀寫(xiě)類(lèi)分享,大家參考使用吧2013-12-12C#開(kāi)源的AOP框架--KingAOP基礎(chǔ)
這篇文章主要介紹了一款C#開(kāi)源的AOP框架--KingAOP框架的基礎(chǔ)知識(shí),對(duì)于想學(xué)習(xí)AOP的小伙伴來(lái)說(shuō),非常不錯(cuò),希望大家能夠喜歡。2015-12-12