聊聊ThreadLocal
- 为什么需要Thread Local Storage
- Thread Local Storage的实现
- PThread库实现
- 操作系统实现
- GCC __thread关键字实现
- C++11 thread_local实现
- JAVA ThreadLocal实现
Thread Local Storage 线程局部存储,简称TLS。
为什么需要Thread Local Storage
变量分为全局变量和局部变量。
- 全局变量:在全局范围内有效,其生命周期跟程序进程的生命周期一致,即在程序启动时初始化,在程序结束时被销毁。
- 局部变量:只在某段代码块内有效,其生命周期是代码块被执行期间,即在进入该段代码块时初始化,离开该段代码块时销毁(对自带垃圾回收的语言,这个销毁会有点滞后)。
全局变量可以用于在多线程间传递数据,非常方便,但需要考虑并发访问冲突问题,一般都需要同步代码块/加锁访问。局部变量只能在代码块内访问,在多线程间互不干扰,无须考虑并发访问冲突问题。
在日常工作中,我们可能会碰到以下场景:希望每个线程拥有自己的变量副本(Thread Local Storage),这样该变量(也称为ThreadLocal变量)在线程间互不干扰,从而避免并发访问冲突问题。
比如随机数生成场景中,生成的伪随机数生成,即当随机数种子固定后,那么生成的随机数序列都是固定的。为了保证随机数的随机性,就可以将随机数种子声明为ThreadLocal,这样在不同的线程中,这些随机数种子不同,从而不同线程生成的随机数序列也不同。
比如linux系统中的errno变量,该变量是全局变量,很早之前都是单线程模型,errno的用法没问题,但后来支持多线程了,errno变量值就受到多线程干扰了,为了保证多线程的errno能正确返回,只能通过Thread Local Storage的方式,无法通过加锁的方式保证。
Thread Local Storage的实现
Thread Local Storage的本质就是每个线程都有该变量副本。
PThread库实现
在C语言中,可以使用Pthread库来实现线程局部存储。
Pthread库提供了一种称为线程特定数据(Thread-Specific Data, TSD)的机制,允许每个线程关联一组键值对。每个线程可以通过键来访问和修改其关联的值,而不会影响其他线程中的相同键的值。
在内部,Pthread库通常会为每个线程维护一个线程局部存储的数据结构(如哈希表),用于存储键值对。每个线程在访问或修改其局部存储的数据时,都会通过这个数据结构进行操作。
为了使用TSD的特性,Pthread库提供了以下方法
//创建键,即获取一个keys数组的索引
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
//设置键关联的数据
int pthread_setspecific(pthread_key_t key, const void *value);
//获取键关联的数据
void *pthread_getspecific(pthread_key_t key);
//释放键,即重置键关联keys数组中对应的值,以便其他变量使用
int pthread_key_delete(pthread_key_t key);
- 键(Key)的创建和管理
使用pthread_key_create函数可以创建一个键,该键可以被多个线程共享。创建键时,可以指定一个析构函数,当线程结束时,该函数会被调用来释放与键关联的数据。 - 数据的设置和获取
使用pthread_setspecific函数可以将数据与特定的键和线程关联起来。使用pthread_getspecific函数可以获取与特定键和线程关联的数据。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 定义一个键
pthread_key_t key;
// 线程函数
void* thread_func(void* arg) {
int* data = (int*)malloc(sizeof(int));
*data = *(int*)arg;
pthread_setspecific(key, data);
// 获取并打印线程局部存储的数据
int* retrieved_data = (int*)pthread_getspecific(key);
printf("Thread %ld: data = %d\n", pthread_self(), *retrieved_data);
return NULL;
}
int main() {
pthread_t thread1, thread2;
int data1 = 10, data2 = 20;
// 创建键
pthread_key_create(&key, free);
// 创建线程
pthread_create(&thread1, NULL, thread_func, &data1);
pthread_create(&thread2, NULL, thread_func, &data2);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁键
pthread_key_delete(key);
return 0;
}
操作系统实现
操作系统在实现Thread Local Storage机制上主要考虑以下方面:
- TLS数据结构的分配
操作系统为每个线程分配一个独立的TLS区域,用于存储该线程的所有TLS变量。在编译期间可以确定TLS变量个数,所以这个区域通常是一个固定大小的内存块。 - TLS变量的存储
每个线程可能会访问多个TLS变量,操作系统会为每个TLS变量分配一个唯一的偏移量,这个偏移量表示该变量在TLS区域中的位置。线程可以通过这个偏移量访问自己的TLS变量。 - TLS变量的访问
当线程需要访问一个TLS变量时,操作系统会提供一组特殊的指令或函数,用于从当前线程的TLS区域中获取该变量的值。这些指令或函数通常会使用线程ID和TLS变量的偏移量来计算变量的实际地址。 - TLS变量的初始化
操作系统会在每个线程开始执行时自动初始化TLS变量。对于全局范围的TLS变量,操作系统会在进程启动时为其分配内存并进行初始化。对于函数范围内的TLS变量,操作系统会在函数调用时为其分配内存并进行初始化。 - TLS变量的销毁
当线程结束时,操作系统会自动回收其TLS区域,并释放相应资源。
GCC __thread关键字实现
GCC通过使用操作系统提供的线程局部存储(Thread Local Storage,TLS)机制来实现**__thread关键字**。__thread关键字用于声明线程局部变量。这些变量在每个线程中都有独立的实例,互不干扰。当线程结束时,这些变量的生命周期也随之结束。
以下是GCC实现__thread关键字的一些关键步骤:
- 生成TLS变量
当你在代码中使用__thread关键字声明一个变量时,GCC会为该变量生成一个TLS符号。这个符号在程序的整个生命周期内都存在,但在不同的线程中具有不同的值。
例如:
__thread int counter = 0;
编译后,GCC会生成一个类似于_ZL7counter的TLS符号。 - 分配TLS空间
在程序启动时,操作系统会为每个线程分配一块TLS空间。这块空间的大小取决于程序中声明的TLS变量的数量。GCC会在程序初始化时计算所需的TLS空间大小,并将其传递给操作系统。Linux默认最大只支持1024个TLS变量。 - 访问TLS变量
当线程访问一个__thread变量时,GCC会生成一段特殊的代码,用于从当前线程的TLS空间中获取该变量的值。这段代码通常是一个内存访问指令,其地址由线程ID和TLS偏移量计算得出。
例如,访问上面的counter变量时,GCC可能会生成类似以下的代码:
movl $_ZL7counter@TLSGD(%rip), %eax
这段代码将当前线程的TLS空间中counter变量的值加载到寄存器%eax中。 - 初始化TLS变量
GCC会在每个线程开始执行时自动初始化__thread变量。对于全局范围的__thread变量,GCC会在程序启动时为其分配内存并进行初始化。对于函数范围内的静态变量,GCC会在首次调用时为其分配内存并进行初始化。 - 销毁TLS变量
当线程结束时,操作系统会自动回收其TLS空间,并释放相应资源。
__thread的使用限制
- 只能修饰POD类型(类似整型指针的标量,不带自定义的构造、拷贝、赋值、析构的类型,二进制内容可以任意复制memset,memcpy,且内容可以复原)。
- 不能修饰class类型,因为无法自动调用构造和析构函数。
- 可用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或class的普通成员变量。
- __thread变量值只能初始化为编译器常量
- __thread限定符(specifier)可以单独使用,也可带有extern或static限定符,但不能带有其它存储类型的限定符。
- __thread可用于全局的静态文件作用域,静态函数作用域或一个类中的静态数据成员。不能用于块作用域,自动或非静态数据成员。
C++11 thread_local实现
c++11提供的thread_local实现跟GCC __thread实现类似,都是借助操作系统的TLS机制实现的。但是c++11提供的thread_local可跨平台使用,也可修饰非POD类型的变量。
#include <iostream>
#include <thread>
// 声明一个线程局部变量
thread_local int thread_local_var = 0;
void thread_function(int thread_id) {
// 更新线程局部变量的值
thread_local_var = thread_id;
std::cout << "Thread " << thread_id << ": thread_local_var = " << thread_local_var << std::endl;
}
int main() {
// 创建两个线程
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);
// 等待线程结束
t1.join();
t2.join();
return 0;
}
JAVA ThreadLocal实现
Java采用的实现方案跟上面类似,也是每个线程一个数组,专门用来存储变量副本。
由图可知,每个线程使用ThreadLocalMap存储ThreadLocal对应的具体值,在读写ThreadLocal变量对应的值时,最终都是到table中读写。由于不同线程的table不一样,虽然ThreadLocal变量一致,但是对应的值不一样,这样就实现了不同线程有不同的数据副本。