千家信息网

如何理解被C#的ThreadStatic标记的静态变量

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,本篇内容主要讲解"如何理解被C#的ThreadStatic标记的静态变量",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"如何理解被C#的ThreadStat
千家信息网最后更新 2026年11月02日如何理解被C#的ThreadStatic标记的静态变量

本篇内容主要讲解"如何理解被C#的ThreadStatic标记的静态变量",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"如何理解被C#的ThreadStatic标记的静态变量"吧!

ThreadStatic 的用法

1. 普通的 static 变量

相信很多朋友在代码中都使用过 static 变量,它的好处多多,比如说我经常会用 static 去做一个进程级缓存,从而提高程序的性能,当然你也可以作为一个非常好的一级缓存,如下代码:


public class Test
{
public static Dictionary cachedDict = new Dictionary();
}

刚才我也说到了,这是一个进程级的缓存,多个线程都看得见,所以在多线程的环境下,你需要特别注意同步的问题。要么使用锁,要么使用 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 cachedDict = new Dictionary();
}

接下来可以开多个线程给 cachedDict 灌数据,看看 dict 是不是 Thread级作用域,实现代码如下:


class Program
{
static void Main(string[] args)
{
var task1 = Task.Run(() =>
{
if (Test.cachedDict == null) Test.cachedDict = new Dictionary();
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();
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 cachedDict = new Dictionary();
}

从结果来看,确实是一个 Thread 级,而且还避免了线程间同步开销,哈哈????,这么神奇的东西,难怪有读者想看看底层到底是怎么实现的。

用 Windbg 挖 ThreadStatic

1. 对 TEB 和 TLS 的认识

  • TEB (Thread Environment Block)

每一个线程都有一份属于自己专属的私有数据,这些数据就放在 Thread 的 TEB 中,如果你想看的话,可以在 windbg 中打印出来。


0:000> !teb
TEB at 0000001e1cdd3000
ExceptionList: 0000000000000000
StackBase: 0000001e1cf80000
StackLimit: 0000001e1cf6e000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 0000001e1cdd3000
EnvironmentPointer: 0000000000000000
ClientId: 0000000000005980 . 0000000000005aa8
RpcHandle: 0000000000000000
Tls Storage: 000001b599d06db0
PEB Address: 0000001e1cdd2000
LastErrorValue: 0
LastStatusValue: c0000139
Count Owned Locks: 0
HardErrorMode: 0

从 teb 的结构中可以看出,既有 线程本地存储(TLS),也有异常相关信息的存储 (ExceptionList) 等等相关信息。

  • TLS (Thread Local Storage)

进程会在启动后给 TLS 分配总共 1088 个槽位,每个线程都会分配一个专属的 tlsindex 索引,并且拥有一组 slots 槽位,可以用 windbg 去验证一下。


0:000> !tls
Usage:
tls [teb]
slot: -1 to dump all allocated slots
{0-0n1088} to dump specific slot
teb: for current thread
0 for all threads in this process
(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 : 0000000000000000

0:000> !t Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 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 line

var 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 的变量就会存放在这个数组的某一个内存块中。

到此,相信大家对"如何理解被C#的ThreadStatic标记的静态变量"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

线程 代码 变量 接下来 标记 地址 思维 数据 方法 进程 静态 C# 作用 信息 内容 朋友 比如说 索引 缓存 同步 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 瑰夏咖啡 互联网科技网 北京路客互联网科技有限公司 港股互联网科技股 猪兼强互联网科技 善班互联网科技 天津摩尔互联网科技 广州善班互联网科技 上游互联网科技 哈尔滨新华互联网科技 荆州市昕露互联网络科技有限公司 开一家科技互联网公司需要多少钱 科技类公司包括互联网吗 互联网新科技动漫ppt素材 南博望互联网科技有限公司 互联网金融科技元年 伍佰万互联网科技怎么赚钱 电子商务与互联网科技的关系 中国互联网公司的科技投入 恒生科技指数 中概互联网 互联网是第几次科技革命的成果 互联网与未来科技 菲比寻常互联网科技 互联网公司会议桌创意科技软装饰 链环科技传统企业怎么转型互联网 深圳育鹏互联网科技有限公司招聘 洛阳共同富裕互联网科技有限公司 厦门江牛科技互联网 福建互联网展示科技馆设计 陕西彩京云互联网科技有限公司
0