一:背景

1. 讲故事

有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的?这么说吧,C#的ThreadStatic是假的,因为C#完全是由CLR(C++)承载的,言外之意C#的线程本地存储,用的就是用C++运行时提供的 __declspec(thread)__thread 来虚构的一套玩法,这一篇我们就来简单聊一聊。

二:C# 的线程本地存储

1. 虚构在哪里

在 C# 中使用ThreadStatic就可以将变量和线程进行绑定,参考代码如下:


internal class Program
{
[ThreadStatic]
public static int num = 10; static void Main(string[] args)
{
Console.WriteLine($"num={num}"); Debugger.Break();
}
}

在 CLR 中如何将 num 与 Thread 绑定呢?研究过 CLR 源码的朋友应该知道是用 ThreadLocalInfo 的,参考代码如下:


#ifdef _MSC_VER
__declspec(selectany) __declspec(thread) ThreadLocalInfo gCurrentThreadInfo;
#else
EXTERN_C __thread ThreadLocalInfo gCurrentThreadInfo;
#endif struct ThreadLocalInfo
{
Thread* m_pThread;
AppDomain* m_pAppDomain; // This field is read only by the SOS plugin to get the AppDomain
void** m_EETlsData; // ClrTlsInfo::data
};

上面的 m_pThread 就是 C# Thread 在 CLR 层面的承载,怎么去验证呢?可以把代码跑起来,然后用 windbg 验证一下。


0:000> dt coreclr!gCurrentThreadInfo
+0x000 m_pThread : 0x000001e3`506c5fa0 Thread
+0x008 m_pAppDomain : 0x000001e3`506ba9b0 AppDomain
+0x010 m_EETlsData : 0x000001e3`506aa360 -> (null) 0:000> !t
ThreadCount: 3
UnstartedThread: 0
BackgroundThread: 2
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 2e04 000001E3506C5FA0 2a020 Preemptive 000001E3521DCE80:000001E3521DD4A8 000001e3506ba9b0 -00001 MTA
6 2 4ef8 000001E3506F1A30 21220 Preemptive 0000000000000000:0000000000000000 000001e3506ba9b0 -00001 Ukn (Finalizer)
7 3 3550 000001E3726A0AE0 2b220 Preemptive 0000000000000000:0000000000000000 000001e3506ba9b0 -00001 MTA

从卦中可以清楚的看到 m_pThread=0x000001e3506c5fa0 就是我们的主线程,最后的 num 就是放在与之关联的 ThreadLocalModule 中,这个比较简单,关注下汇编代码就好了,下面的 rax 就是 ThreadLocalModule。


00007ffb`218d2c2c 48b9b07b9921fb7f0000 mov rcx,7FFB21997BB0h
00007ffb`218d2c36 ba04000000 mov edx,4
00007ffb`218d2c3b e8001fb55f call coreclr!JIT_GetSharedNonGCThreadStaticBase (00007ffb`81424b40)
00007ffb`218d2c40 8b4820 mov ecx,dword ptr [rax+20h]
00007ffb`218d2c43 894dfc mov dword ptr [rbp-4],ecx 0:000> dp rax+0x20 L1
00000294`d0539790 abababab`0000000a

CLR层面用了太多的高层虚构来玩了一套线程本地存储,其实最核心的还要理解再下一层的 __declspec(selectany) ,接下来聊聊这玩意是怎么玩的。

2. __declspec(selectany) 是怎么玩的

在Windows层面的术语中,有两种 TLS 技术。

  • 动态TLS

借助 Windows 提供的 TlsAlloc, TlsSetValue 之类的方法来实现,并且存放在线程 _TEB.TlsSlots 的槽位中,参考代码如下:


0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
+0x000 NtTib : _NT_TIB
...
+0x1480 TlsSlots : [64] (null)
...
  • 静态TLS

C#的线程本地存储用的就是静态TLS,也就是在编译时就已经声明好的,在 PE 文件里面有一个 .tls 节点,这个节点的数据会被每个线程在heap堆上copy一份,存放在 _TEB.ThreadLocalStoragePointer 来指向的指针数组中,参考代码如下:


0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x058 ThreadLocalStoragePointer : 0x00000294`d0536ab0 Void
...

动态的TLS我就不介绍了,这里着重说一下静态的TLS。

3. 静态TLS详解

为了方便讲解,先上一段测试代码。


#include <windows.h>
#include <stdio.h>
#include <limits.h> __declspec(thread) int i = INT_MAX;
__declspec(thread) int j = INT_MAX; int main() {
int num1 = i;
int num2 = j;
printf("i=%d,j=%d", num1, num2);
}

上面的 i,j 值在编译时就已经放到了 PE 头的 .tls 节,可以用 PPEE 观察下对象头。

从卦中可以看到 .tls 占用了 0x400 字节大小,并且用 WinHex 真的观察到了 i,j 的值,挺有意思。

在内存中TLS区比这个还小一点,可以观察一下 DIRECTORY_ENTRY_TLS 节的 StartAddressOfRawData 和 EndAddressOfRawData 字段,这也是每个线程copy的原始内存区域,可以看到只有 0x20D ,大概少了一半,截图如下:

有了这些前置知识,接下来观察内存中的地址,在运行之前先把 ASLR 关掉,汇编代码参考如下:

   //int num1 = i;
14 00411895 a1b4a14100 mov eax,dword ptr [ConsoleApplication2!_tls_index (0041a1b4)]
14 0041189a 648b0d2c000000 mov ecx,dword ptr fs:[2Ch]
14 004118a1 8b1481 mov edx,dword ptr [ecx+eax*4]
14 004118a4 8b8208010000 mov eax,dword ptr [edx+108h]
14 004118aa 8945f8 mov dword ptr [ebp-8],eax //int num2 = j;
15 004118ad a1b4a14100 mov eax,dword ptr [ConsoleApplication2!_tls_index (0041a1b4)]
15 004118b2 648b0d2c000000 mov ecx,dword ptr fs:[2Ch]
15 004118b9 8b1481 mov edx,dword ptr [ecx+eax*4]
15 004118bc 8b8204010000 mov eax,dword ptr [edx+104h]
15 004118c2 8945ec mov dword ptr [ebp-14h],eax

可以看到每一句大概会生成 5 行汇编代码,我们简单分析下。

  • ConsoleApplication2!_tls_index (0041a1b4)

这个值就是 PE 头的 AddressOfIndex 值,可以再回头观察下,里面存的就是 tls 索引,当前是 0 ,参考如下:


0:000> dp 0041a1b4 L1
0041a1b4 00000000
  • fs:[2Ch]

在用户态层面上 fs 指向的是当前线程的 TEB 结构,其中的 2C 偏移指的就是 ThreadLocalStoragePointer 结构,windbg 观察如下:


0:000> dg fs
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0053 002bc000 00000fff Data RW Ac 3 Bg By P Nl 000004f3 0:000> dt 0x002bc000 ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : (null)
+0x02c ThreadLocalStoragePointer : 0x00664400 Void
...
  • edx,dword ptr [ecx+eax*4]

这句汇编是一个数组操作,翻译成 C 就是 ThreadLocalStoragePointer[tls]


0:000> dp 0x00664400 L1
00664400 00664448

这里要提醒的是:上面的 00664448 所在的 heap 位置其实就是 PE 头里的 StartAddressOfRawData~EndAddressOfRawData内存区域的 copy,截图如下:

  • eax,dword ptr [edx+108h]

这句话的意思就是在 数组元素1 这个结构上偏移108的位置存放着我们的 num 值,用 windbg 观察之后果然就是的。


0:000> dp 00664448+0x108 L1
00664550 7fffffff

三:总结

C# 属于一种业务高层抽象的语言,它的很多底层被C++再次隔离了,想要理解本篇的TLS,还得需要往下一层一层的击穿,作为C#程序员太难了。

聊一聊 C# 的线程本地存储TLS到底是什么的更多相关文章

  1. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    原文链接地址:http://www.cppblog.com/Tim/archive/2012/07/04/181018.html 本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们 ...

  2. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们知道在一个进程中,所有线程是共享同一个地址空间的.所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线 ...

  3. Atitit usrqbg1821 Tls 线程本地存储(ThreadLocal Storage 规范标准化草案解决方案ThreadStatic

    Atitit usrqbg1821 Tls 线程本地存储(ThreadLocal Storage 规范标准化草案解决方案ThreadStatic 1.1. ThreadLocal 设计模式1 1.2. ...

  4. 线程本地存储(Thread Local Storage, TLS)简单分析与使用

    在多线程编程中, 同一个变量, 如果要让多个线程共享访问, 那么这个变量可以使用关键字volatile进行声明; 那么如果一个变量不想使多个线程共享访问, 那么该怎么办呢? 呵呵, 这个办法就是TLS ...

  5. 线程本地存储(动态TLS和静态TLS)

    线程本地存储(TLS) 对于多线程应用程序,如果线程过于依赖全局变量和静态局部变量就会产生线程安全问题.也就是一个线程的使用全局变量可能会影响到其他也使用此全局变量的线程,有可能会造成一定的错误,这可 ...

  6. .NET:线程本地存储、调用上下文、逻辑调用上下文

    .NET:线程本地存储.调用上下文.逻辑调用上下文 目录 背景线程本地存储调用上下文逻辑调用上下文备注 背景返回目录 在多线程环境,如果需要将实例的生命周期控制在某个操作的执行期间,该如何设计?经典的 ...

  7. C# 线程本地存储 调用上下文 逻辑调用上下文

    线程本地存储 using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleAppTest ...

  8. ThreadLocal(线程本地存储)

    1. ThreadLocal,即线程本地变量或线程本地存储. threadlocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的 ...

  9. 线程本地存储 ThreadLocal

    线程本地存储 · 语雀 (yuque.com) 线程本地存储提供了线程内存储变量的能力,这些变量是线程私有的. 线程本地存储一般用在跨类.跨方法的传递一些值. 线程本地存储也是解决特定场景下线程安全问 ...

  10. Java线程本地存储ThreadLocal

    前言 ThreadLocal 是一种 无同步 的线程安全实现 体现了 Thread-Specific Storage 模式:即使只有一个入口,内部也会为每个线程分配特有的存储空间,线程间 没有共享资源 ...

随机推荐

  1. iOS16新特性:实时活动-在锁屏界面实时更新APP消息

    简介 之前在 <iOS16新特性:灵动岛适配开发与到家业务场景结合的探索实践> 里介绍了iOS16新的特性:实时更新(Live Activity)中灵动岛的适配流程,但其实除了灵动岛的展示 ...

  2. antd/fusion表格增加圈选复制功能

    背景介绍 我们存在着大量在PC页面通过表格看数据业务场景,表格又分为两种,一种是 antd / fusion 这种基于 dom 元素的表格,另一种是通过 canvas 绘制的类似 excel 的表格. ...

  3. IDEA降低注解检测级别

    在 File | Settings | Editor | Inspections 选项中使用搜索功能找到 Autowiring for Bean Class,将 Severity 的级别由之前的 er ...

  4. Python中的可迭代对象和迭代器

    1.可迭代对象 1.1.可迭代对象概念 可迭代对象,最直观的感觉就是可以使用for来循环迭代每一个元素.例如Python内置的类型:str.list.tuple.dict等类型的对象,都是可迭代对象. ...

  5. 安信可开发环境构建-基于Ai-WB2系列 和 Ai-M61 或 Ai-M62 (环境上下文切换)

    首先,对于Ai-WB2系列环境的构建官方文档已经讲的非常明白了,这里不做阐述如下链接所示https://blog.csdn.net/Boantong_/article/details/12848091 ...

  6. Vue源码学习(十三):实现watch(一):方法,对象

    好家伙,  代码出了点bug,暂时只能实现这两种形式 完整代码已开源https://github.com/Fattiger4399/analytic-vue.git Vue:watch的多种使用方法 ...

  7. QSplitter 自我总结

    ①.QSplitter(QWidget* parent = Q_NULLPTR); //构造函数 QSplitter(Qt::Orientation orientation , QWidget* pa ...

  8. [Python]常用知识

    Python 常用知识 编译型语言 和 解释性语言 解释性语言 编译型语言 概念 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的 ...

  9. Langchain-Chatchat项目:4.1-P-Tuning v2实现过程

      常见参数高效微调方法(Parameter-Efficient Fine-Tuning,PEFT)有哪些呢?主要是Prompt系列和LoRA系列.本文主要介绍P-Tuning v2微调方法.如下所示 ...

  10. Redis创始人开源最小聊天服务器,仅200行代码,几天功夫已获2.8K Star!

    中午时候,在技术交流群里聊起关于Redis创始人的一些趣事,比如离开Redis之后,去写科幻小说之类的. 因为好奇科幻小说,TJ君就去搜索了一下.结果一搜,发现Redis作者最近居然又搞了个新活儿! ...