被 C# 的 ThreadStatic 标记的静态变量,都存放在哪里了?
一:背景
1. 講故事
前幾天公號里有一位朋友留言說,你windbg玩的溜,能幫我分析下被 ThreadStatic 修飾的變量到底存放在哪里嗎?能不能幫我挖出來????????????,其實這個問題問的挺深的,玩高級語言的朋友相信很少有接觸到這個的,雖然很多朋友都知道這個特性怎么用,當然我也沒特別研究這個,既然要回答這個問題,我得研究研究回答之!為了更好的普適性,先從簡單的說起!
二:ThreadStatic 的用法
1. 普通的 static 變量
相信很多朋友在代碼中都使用過 static 變量,它的好處多多,比如說我經常會用 static 去做一個進程級緩存,從而提高程序的性能,當然你也可以作為一個非常好的一級緩存,如下代碼:
public?class?Test{public?static?Dictionary<int,?string>?cachedDict?=?new?Dictionary<int,?string>();}剛才我也說到了,這是一個進程級的緩存,多個線程都看得見,所以在多線程的環境下,你需要特別注意同步的問題。要么使用鎖,要么使用 ConcurrentDictionary,我覺得這也是一個思維定式,很多時候思維總是在現有基礎上去修補,去亡羊補牢,而沒有跳出這個思維從根基上去處理,說這么多是什么意思呢?我舉一個例子:
在市面上常見的鏈式跟蹤框架中,比如說:Zikpin,SkyWalking,會使用一些集合去存儲跟蹤當前線程的一些鏈路信息,比如說 A -> B -> C -> D -> B -> A,常規的思維就像上面說的一樣,定義一個全局 cachedDict,然后使用各種同步機制,其實你也可以降低 cachedDict 的訪問作用域,將 全局訪問 改成 Thread級訪問,這難道不是更好的解決思路嗎?
2. 用 ThreadStatic 標記 static 變量
要想做到 Thread級作用域,實現起來非常簡單,在 cachedDict 上打一個 ThreadStatic 特性即可,修改代碼如下:
public?class?Test{[ThreadStatic]public?static?Dictionary<int,?string>?cachedDict?=?new?Dictionary<int,?string>();}接下來可以開多個線程給 cachedDict 灌數據,看看 dict 是不是 Thread級作用域,實現代碼如下:
class?Program{static?void?Main(string[]?args){var?task1?=?Task.Run(()?=>{if?(Test.cachedDict?==?null)?Test.cachedDict?=?new?Dictionary<int,?string>();Test.cachedDict.Add(1,?"mary");Test.cachedDict.Add(2,?"john");Console.WriteLine($"thread={Thread.CurrentThread.ManagedThreadId}?的?dict?有記錄:?{Test.cachedDict.Count}");});var?task2?=?Task.Run(()?=>{if?(Test.cachedDict?==?null)?Test.cachedDict?=?new?Dictionary<int,?string>();Test.cachedDict.Add(3,?"python");Test.cachedDict.Add(4,?"jaskson");Test.cachedDict.Add(5,?"elen");Console.WriteLine($"thread={Thread.CurrentThread.ManagedThreadId}?的?dict?有記錄:?{Test.cachedDict.Count}");});Console.ReadLine();}}public?class?Test{[ThreadStatic]public?static?Dictionary<int,?string>?cachedDict?=?new?Dictionary<int,?string>();}從結果來看,確實是一個 Thread 級,而且還避免了線程間同步開銷,哈哈????,這么神奇的東西,難怪有讀者想看看底層到底是怎么實現的。
三:用 Windbg 挖 ThreadStatic
1. 對 TEB 和 TLS 的認識
TEB (Thread Environment Block)
每一個線程都有一份屬于自己專屬的私有數據,這些數據就放在 Thread 的 TEB 中,如果你想看的話,可以在 windbg 中打印出來。
0:000>?!teb TEB?at?0000001e1cdd3000ExceptionList:????????0000000000000000StackBase:????????????0000001e1cf80000StackLimit:???????????0000001e1cf6e000SubSystemTib:?????????0000000000000000FiberData:????????????0000000000001e00ArbitraryUserPointer:?0000000000000000Self:?????????????????0000001e1cdd3000EnvironmentPointer:???0000000000000000ClientId:?????????????0000000000005980?.?0000000000005aa8RpcHandle:????????????0000000000000000Tls?Storage:??????????000001b599d06db0PEB?Address:??????????0000001e1cdd2000LastErrorValue:???????0LastStatusValue:??????c0000139Count?Owned?Locks:????0HardErrorMode:????????0從 teb 的結構中可以看出,既有 線程本地存儲(TLS),也有異常相關信息的存儲 (ExceptionList) 等等相關信息。
TLS (Thread Local Storage)
進程會在啟動后給 TLS 分配總共 1088 個槽位,每個線程都會分配一個專屬的 tlsindex 索引,并且擁有一組 slots 槽位,可以用 windbg 去驗證一下。
0:000>?!tls Usage: tls?<slot>?[teb]slot:??-1?to?dump?all?allocated?slots{0-0n1088}?to?dump?specific?slotteb:???<empty>?for?current?thread0?for?all?threads?in?this?process<teb?address>?(not?threadid)?to?dump?for?specific?thread. 0:000>?!tls?-1 TLS?slots?on?thread:?5980.5aa8 0x0000?:?0000000000000000 0x0001?:?0000000000000000 0x0002?:?0000000000000000 0x0003?:?0000000000000000 0x0004?:?0000000000000000 ... 0x0019?:?0000000000000000 0x0040?:?00000000000000000:000>?!t????????????????????????????????????????????????????????????????????????????????????????????????????????Lock??DBG???ID?OSID?ThreadOBJ???????????State?GC?Mode?????GC?Alloc?Context??????????????????Domain???????????Count?Apt?Exception0????1?5aa8?000001B599CEED90????2a020?Preemptive??000001B59B9042F8:000001B59B905358?000001b599cdb130?1?????MTA?5????2??90c?000001B599CF4930????2b220?Preemptive??0000000000000000:0000000000000000?000001b599cdb130?0?????MTA?(Finalizer)?7????3???74?000001B59B7272A0??102a220?Preemptive??0000000000000000:0000000000000000?000001b599cdb130?0?????MTA?(Threadpool?Worker)?9????4?2058?000001B59B7BAFF0??1029220?Preemptive??0000000000000000:0000000000000000?000001b599cdb130?0?????MTA?(Threadpool?Worker)?從上面的 {0-0n1088} to dump specific slot 中可以看出,進程中總會有 1088 個槽位,而且當前主線程 5aa8 擁有 27 個 slot 槽位。
好了,基本概念介紹完了,接下來準備分析一下匯編代碼了。
2. 從匯編代碼中尋找答案
為了更好的用 windbg 去挖,我就定義一個簡單的 ThreadStatic int 變量,代碼如下:
class?Program{[ThreadStatic]public?static?int?i?=?0;static?void?Main(string[]?args){i?=?10;???//?12?linevar?num?=?i;Console.ReadLine();}}接下來用 !U 反匯編一下 Main 函數的代碼,著重看一下第 12 行代碼的 i = 10;。
0:000>?!U?/d?00007ffbe0ae0ffb E:\net5\ConsoleApp5\ConsoleApp5\Program.cs?@?12: 00007ffb`e0ae0fd6?48b9b0fbb7e0fb7f0000?mov?rcx,7FFBE0B7FBB0h 00007ffb`e0ae0fe0?ba01000000??????mov?????edx,1 00007ffb`e0ae0fe5?e89657a95f??????call????coreclr!JIT_GetSharedNonGCThreadStaticBase?(00007ffc`40576780) 00007ffb`e0ae0fea?c7401c0a000000??mov?????dword?ptr?[rax+1Ch],0Ah從匯編指令上來看,最后的 10 賦給了 rax+1Ch 的低32位,那 rax 的地址從哪里來的呢?可以看出核心邏輯在 JIT_GetSharedNonGCThreadStaticBase 方法內,接下來就得研究一下這個方法都干嘛了。
3. 調試核心函數 JIT_GetSharedNonGCThreadStaticBase
接下來在第 12 處設置一個斷點 !bpmd Program.cs:12 處,方法的簡化匯編代碼如下:
coreclr!JIT_GetSharedNonGCThreadStaticBase: 00007ffc`2c38679a?448b0dd7894300?????????mov?????r9d,?dword?ptr?[coreclr!_tls_index?(00007ffc`2c7bf178)] 00007ffc`2c3867a1?654c8b042558000000?????mov?????r8,?qword?ptr?gs:[58h] 00007ffc`2c3867aa?b908000000?????????????mov?????ecx,?8 00007ffc`2c3867af?4f8b04c8???????????????mov?????r8,?qword?ptr?[r8+r9*8] 00007ffc`2c3867b3?4e8b0401???????????????mov?????r8,?qword?ptr?[rcx+r8] 00007ffc`2c3867b7?493b8060040000?????????cmp?????rax,?qword?ptr?[r8+460h] 00007ffc`2c3867be?732b???????????????????jae?????coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b?(00007ffc`2c3867eb) 00007ffc`2c3867c0?4d8b8058040000?????????mov?????r8,?qword?ptr?[r8+458h] 00007ffc`2c3867c7?498b04c0???????????????mov?????rax,?qword?ptr?[r8+rax*8] 00007ffc`2c3867cb?4885c0?????????????????test????rax,?rax 00007ffc`2c3867ce?741b???????????????????je??????coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b?(00007ffc`2c3867eb) 00007ffc`2c3867d0?8bca???????????????????mov?????ecx,?edx 00007ffc`2c3867d2?f644011801?????????????test????byte?ptr?[rcx+rax+18h],?1 00007ffc`2c3867d7?7412???????????????????je??????coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b?(00007ffc`2c3867eb) 00007ffc`2c3867d9?488b4c2420?????????????mov?????rcx,?qword?ptr?[rsp+20h] 00007ffc`2c3867de?4833cc?????????????????xor?????rcx,?rsp 00007ffc`2c3867e1?e89a170600?????????????call????coreclr!__security_check_cookie?(00007ffc`2c3e7f80) 00007ffc`2c3867e6?4883c438???????????????add?????rsp,?38h 00007ffc`2c3867ea?c3?????????????????????ret??接下來我仔細分析下這里的 mov 操作。
1) dword ptr [coreclr!_tls_index (00007ffc`2c7bf178)]
這個很簡單,獲取該線程專屬的 tls_index 索引
2) qword ptr gs:[58h]
這里的 gs:[58h] 是什么意思呢?應該有朋友知道,gs寄存器 是專門用于存放當前線程的 teb 地址,后面的 58 表示在 teb 地址上的偏移量,那問題來了,這個地址到底指向誰了呢?其實你可以把 teb 的數據結構給打印出來就明白了。
0:000>?dt?teb coreclr!TEB+0x000?NtTib????????????:?_NT_TIB+0x038?EnvironmentPointer?:?Ptr64?Void+0x040?ClientId?????????:?_CLIENT_ID+0x050?ActiveRpcHandle??:?Ptr64?Void+0x058?ThreadLocalStoragePointer?:?Ptr64?Void+0x060?ProcessEnvironmentBlock?:?Ptr64?_PEB...上面這句 +0x058 ThreadLocalStoragePointer : Ptr64 Void 可以看出,其實就是指向 ThreadLocalStoragePointer 。
3) qword ptr [r8+r9*8]
有了前兩步的基礎,這句匯編就很簡單了,它做了一個索引操作: ThreadLocalStoragePointer[tls_index] ,對不對,從而獲取屬于該線程的 tls 內容,這個 ThreadStatic 的變量就會存放在這個數組的某一個內存塊中。
后續還有一些計算偏移的邏輯運算都基于這個 ?ThreadLocalStoragePointer[tls_index] 之上,方法調用繞來繞去,匯編沒法看哈 ????????????
四:總結
總的來說,可以確定 ThreadStatic 變量 確實是存放在 TEB 的 ThreadLocalStoragePointer 數組中,這幾天 NET5 的 CoreCLR 沒有編譯成功,大家如果感興趣,可以 調試 CoreCLR + 匯編 做更深入的挖掘!
總結
以上是生活随笔為你收集整理的被 C# 的 ThreadStatic 标记的静态变量,都存放在哪里了?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个情怀引发的生产事故
- 下一篇: 真正的高手,都有增长思维!(深度好文)