C++ Thread Local Storage




  • Quote 
    • From : http://ju.outofmemory.cn/entry/66238 

我查阅了相关的资料, 发现线程局部存储(TLS)是一个后来者, 产生于多线程概念之后.而在软件发展的早期, 全局变量经常用在库函数中, 用于存储全局信息, 比如errno, 多线程程序产生之后, 全局变量errno就成为所有线程都共享的一个变量, 而实际上, 每个线程都想维护一份自己的errno, 隔离于其他线程.

这个时候, 没人愿意去修改库函数的接口. 于是线程局部存储就诞生了, 根据wikipedia的介绍

Thread-local storage (TLS) is a computer programming method that uses static or global memory local to a thread.

为了在各个平台上都能用上线程局部变量, POSIX Thread定义了一组接口, 用于显式构造使用线程局部存储.

#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

显式构造线程局部变量的方法, 有一个显著优点就是能注册各种类型的对象, 包括内置对象和自定义对象. 而且对象的销毁方式destructor可以显式告诉pthread_key_create, 这样线程退出的时候, 线程局部变量就可以正常销毁, 不至于造成内存泄露.

优点再多, 也禁不住它太难用了, 于是有人就想在编译器添加新功能, 支持特定关键字__thread, 隐式构造线程局部变量

 __thread int i;
 extern __thread struct state s;
 static __thread char *p;

这样的方式, 使用起来是很方便, 但是需要操作系统, 编译器, 连接器, glibc要相应做出修改, 甚至ELF文件格式都需要调整, 这个Ulrich Drepper在tls.pdf中做了详细的介绍.

另一方面, __thread只支持POD类型, 不能用于定义STL中的容器和类, 比如std::string. 非要这么做, 编译器会报错:

main.cpp:8: error: a cannot be thread-local because it has non-POD type std::string
main.cpp:8: error: a is thread-local and so cannot be dynamically initialized

gcc也在文档中专门谈到了Thread-Local, 提到了_thread修饰的变量只能做static initialize

In C++, if an initializer is present for a thread-local variable, it must be a constant-expression, as defined in 5.19.2 of the ANSI/ISO C++ standard.

既然线程局部存储有两种使用方式, 而且各有优缺点, 就有人提出结合二者, 开发一个使用更方便, 又能支持non-POD类型的实现库. 比如 blog 线程局部变量与 __thread

C++11也意识到这个问题, 于是在C++11中引入了新的关键字thread_localDestructor support for thread_local variables介绍说:

One of the key features is that they support non-trivial constructors and destructors that are called on first-use and thread exit respectively.

除了支持non-POD类型的线程局部变量, 它还提到了上文提到的线程局部变量和动态加载so的问题

The compiler can handle constructors, but destructors need more runtime context. It could be possible that a dynamically loaded library defines and constructs a thread_local variable, but is dlclose()’d before thread exit. The destructor of this variable will then have the rug pulled from under it and crash.

解决的思路是实现函数__cxa_thread_atexit_impl(), 供libstdc++在构造对象的时候调用

int __cxa_thread_atexit_impl (void (*dtor) (void *), void *obj,
                          void *dso_symbol);

连接器(ld)为dso_symbol所属的so维护一个引用计数, 维护由它定义的线程局部变量个数. 如果某个线程局部变量被析构, 那引用计数相应减1, 只有引用计数等于0, dlclose()才能卸载so.

其他视角

Walter Bright 在文章It’s Not Always Nice To Share中认为现有的线程局部变量实现都不友好, 在多线程环境下, staticglobal的变量应该默认就是TLS的, 而不是shared, 这样单核时代的代码, 比如C运行库,不用改动就可以运行在多线程环境中; 如果应用程序非要全局shared的变量, 那应该加上shared关键字以明确指明.

总结

再回头看看上文的建议, 如果非要在动态加载so中使用线程局部变量.

  1. 显式线程局部变量:
    • pthread_key_create注册了destructor, 在dlclose()调用之前, 确保调用pthread_key_delete()删除线程局部变量
    • pthread_key_create()中的destructor置为NULL.
  2. 隐式线程局部变量: 因为只支持POD类型, 所以可以用在动态加载so中.


  • Methods  
    • thread_local (c++11)



Refferencs:

  • http://blog.youkuaiyun.com/cywosp/article/details/26876231 
  • http://blog.youkuaiyun.com/cywosp/article/details/26469435 
  • http://ju.outofmemory.cn/entry/66238 
  • http://blog.youkuaiyun.com/u013390476/article/details/52129607 



<think>好的,我现在需要回答用户关于Linux下C++中线程本地存储(Thread Local Storage, TLS原理的问题。用户之前已经询问过ThreadLocal在Java中的实现,现在转向C++在Linux下的实现原理。我需要确保自己理解TLS的概念、实现机制,以及它在Linux环境下的具体工作方式。 首先,我要回忆一下TLS的基本概念。TLS允许每个线程拥有变量的独立副本,这在多线程编程中非常重要,可以避免共享数据导致的竞争条件。在C++中,使用`thread_local`关键字来声明线程本地变量。但用户具体关心的是其底层原理,尤其是在Linux系统中的实现方式。 接下来,我需要考虑Linux下TLS的实现机制。记得Linux主要使用pthread库(如glibc的NPTL实现)来支持多线程,而TLS的实现可能与编译器和操作系统的ABI(Application Binary Interface)有关。可能涉及到的机制包括: 1. **存储模型**:线程本地变量是如何在内存中分配的。每个线程是否有独立的存储区域,例如使用线程控制块(TCB)或者特定的段(如.tdata和.tbss段)来存放这些变量。 2. **访问方式**:线程如何高效访问自己的本地变量。可能需要通过某种指针或寄存器(如FS或GS寄存器在x86架构中的使用)来访问线程特定的内存区域。 3. **动态TLS和静态TLS**:静态TLS是在程序启动时分配的,而动态TLS可以在运行时动态分配。需要区分这两种情况,并说明它们在Linux下的处理方式。 4. **编译器与链接器的角色**:C++中的`thread_local`关键字如何被编译器翻译成特定的存储类别,链接器如何处理这些变量,生成相应的ELF段(如.tdata、.tbss)。 5. **运行时库的支持**:如glibc中的函数(如pthread_key_create, pthread_setspecific, pthread_getspecific)如何管理动态TLS,以及静态TLS的初始化过程。 可能还需要讨论不同架构(如x86_64和ARM)的实现差异,以及线程创建时如何复制或初始化TLS变量。此外,需要考虑性能问题,比如静态TLS访问通常比动态TLS更快,因为可以通过固定的偏移量直接访问,而动态TLS可能需要通过查找表。 另外,用户可能想知道C++11引入的`thread_local`与之前GCC的`__thread`修饰符之间的关系,以及它们在不同标准下的支持情况。 需要验证一些关键点,例如: - 在ELF格式中,TLS变量通常位于.tdata(已初始化)和.tbss(未初始化)段。每个线程在创建时,会为这些段分配内存,并通过特定的段寄存器(如FS或GS)来访问它们。 - 访问静态TLS变量时,编译器生成指令会通过段寄存器加上固定偏移来获取变量的地址。例如,x86_64中使用%fs基址加上偏移量。 - 动态TLS使用键(key)来标识不同的变量,线程通过pthread接口来设置和获取特定于线程的值。这通常涉及哈希表或数组查找,因此效率较低。 需要确保这些信息准确,可能需要查阅相关资料或源码,比如glibc中TLS的实现,或者编译器(如GCC)如何处理`thread_local`关键字。 另外,需要注意用户的问题是关于Linux下C++的实现,因此需要明确区分C++语言层面的特性和系统/编译器层面的实现细节。例如,`thread_local`是C++11标准引入的,而GCC的`__thread`是更早期的扩展,但它们在底层可能共享类似的机制。 可能的误区包括将动态TLS和静态TLS混淆,或者错误地解释寄存器的使用方式。需要确保在解释时清晰地区分不同的情况,并指出它们的优缺点,比如静态TLS的效率更高但数量有限,而动态TLS更灵活但较慢。 最后,需要组织这些信息,以结构化的方式呈现给用户,先介绍基本概念,再深入实现原理,包括存储方式、访问机制、静态与动态TLS的区别,以及相关的编译器与系统支持。</think>### Linux C++ 线程本地存储(TLS原理详解 --- #### **1. 线程本地存储(TLS)的核心概念** - **目标**:为每个线程提供独立的变量副本,避免多线程共享数据时的竞争。 - **C++关键字**:`thread_local`(C++11 标准引入)或编译器扩展(如 GCC 的 `__thread`)。 - **底层实现依赖**: - **编译器**(如 GCC、Clang)生成特定指令和内存布局。 - **操作系统**(Linux 的线程模型,如 NPTL)提供线程管理支持。 - **ELF 文件格式**(通过 `.tdata`、`.tbss` 等段管理静态 TLS)。 --- #### **2. TLS 的两种实现模式** ##### **2.1 静态 TLS(Static TLS)** - **特点**: - 变量在程序启动时分配,数量固定(由 ELF 文件的 TLS 模板决定)。 - 访问速度快(直接通过段寄存器 + 偏移量访问)。 - **实现机制**: - **内存布局**: - **`.tdata` 段**:已初始化的 TLS 变量。 - **`.tbss` 段**:未初始化的 TLS 变量。 - **线程创建时**:每个线程从 TLS 模板复制初始值到线程私有存储区。 - **访问方式**(以 x86_64 为例): ```asm mov %fs:0x10, %rax # 通过 FS 寄存器 + 偏移量访问 TLS 变量 ``` ##### **2.2 动态 TLS(Dynamic TLS)** - **特点**: - 变量在运行时动态分配(如 `pthread_key_create`)。 - 灵活性高,但访问速度较慢(需通过键值查找)。 - **实现机制**: - **键值管理**:每个线程维护一个 TLS 数据指针数组,通过键(`pthread_key_t`)索引。 - **操作函数**: ```c pthread_key_create(&key, destructor); // 创建键 pthread_setspecific(key, value); // 设置线程本地值 void* value = pthread_getspecific(key); // 获取线程本地值 ``` - **内存开销**:每个线程需为动态 TLS 维护独立的数据指针数组。 --- #### **3. 静态 TLS 的底层细节(以 Linux x86_64 为例)** ##### **3.1 内存与寄存器** - **FS/GS 寄存器**: - 在 x86_64 中,`FS` 或 `GS` 寄存器指向当前线程的 TLS 存储基址。 - Linux 默认使用 `FS` 寄存器管理 TLS。 - **访问指令**: ```asm mov %fs:0x10, %rax // 读取 TLS 变量(偏移量 0x10) ``` ##### **3.2 TLS 变量的初始化** - **主线程**:直接使用 ELF 文件中的 `.tdata` 和 `.tbss` 段初始化。 - **新线程**(通过 `pthread_create`): 1. 从主线程的 TLS 模板复制初始值。 2. 分配独立的 TLS 存储区,并设置 `FS` 寄存器指向该区域。 ##### **3.3 性能优势** - **直接寻址**:无哈希计算或链表遍历,访问速度与全局变量接近。 - **缓存友好**:连续内存布局减少缓存未命中。 --- #### **4. 动态 TLS 的实现(以 pthread 库为例)** ##### **4.1 键值存储结构** - **全局键表**:记录所有已分配的键及其析构函数。 - **线程私有数据**:每个线程维护一个 `void*` 数组,通过键的索引访问值。 ##### **4.2 操作流程** 1. **创建键**: ```c pthread_key_create(&key, destructor); ``` - 分配一个未使用的键索引,注册析构函数。 2. **设置值**: ```c pthread_setspecific(key, value); ``` - 将值存入当前线程的 TLS 数组的对应索引位置。 3. **获取值**: ```c void* value = pthread_getspecific(key); ``` - 从当前线程的 TLS 数组的对应索引位置读取值。 ##### **4.3 性能瓶颈** - **间接访问**:需通过键索引多级寻址,速度慢于静态 TLS。 - **锁竞争**:全局键表的操作可能涉及锁(尽管现代实现通常优化为无锁结构)。 --- #### **5. 静态 TLS 与动态 TLS 的对比** | **特性** | **静态 TLS** | **动态 TLS** | |-------------------|---------------------------------------|---------------------------------------| | **分配时机** | 程序启动时 | 运行时动态分配 | | **访问速度** | 快(直接寄存器寻址) | 慢(需键值查找) | | **灵活性** | 固定数量,需提前定义 | 支持动态创建和销毁 | | **适用场景** | 高频访问的线程本地变量 | 需要动态管理的临时数据 | --- #### **6. C++ `thread_local` 关键字的实现** - **编译器行为**: - 若变量在编译期可知大小且数量固定,优先使用静态 TLS。 - 否则(如动态加载的库中使用 `thread_local`),退化为动态 TLS。 - **示例代码**: ```cpp thread_local int val = 42; // 静态 TLS(若编译器支持) ``` - **与 `__thread` 的关系**: - GCC 的 `__thread` 是 C++11 前对静态 TLS 的扩展,行为与 `thread_local` 类似。 - C++11 标准化后,`thread_local` 兼容性更好。 --- #### **7. 常见问题与优化** - **TLS 的数量限制**: - 静态 TLS 的段大小有限(依赖 ELF 模板),过多变量可能导致链接错误。 - 动态 TLS 无硬性限制,但需注意内存开销。 - **性能优化**: - 优先使用静态 TLS 存储高频访问的变量。 - 避免在关键路径频繁调用 `pthread_getspecific`。 --- ### 总结 - **静态 TLS**:通过 ELF 段和寄存器直接寻址实现高效访问,适用于固定变量。 - **动态 TLS**:基于键值管理的灵活方案,适用于运行时动态需求。 - **C++ `thread_local`**:由编译器根据上下文选择最优实现,结合两种 TLS 机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值