当前位置: 首页 > news >正文

聊聊Thread Local Storage

聊聊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采用的实现方案跟上面类似,也是每个线程一个数组,专门用来存储变量副本。
ThreadLocal实现
由图可知,每个线程使用ThreadLocalMap存储ThreadLocal对应的具体值,在读写ThreadLocal变量对应的值时,最终都是到table中读写。由于不同线程的table不一样,虽然ThreadLocal变量一致,但是对应的值不一样,这样就实现了不同线程有不同的数据副本。


http://www.mrgr.cn/news/31481.html

相关文章:

  • 02.06、回文链表
  • Track 09:X-XMCL
  • 十种果冻的做法
  • Python习题 191:将英文句子转为每个单词组成的列表
  • 秒懂Linux之消息队列与信号量(了解)
  • 酸枣病虫害智能化防控系统的探索与实践,基于YOLOv5全系列【n/s/m/l/x】参数模型开发构建枣类作物种植场景下酸枣病虫害智能检测识别系统
  • 第二十一节:学习Redis缓存数据库的Hash操作(自学Spring boot 3.x的第五天)
  • go语言Map详解
  • 深度学习简历面试知识——transformer、VGGish、K-means、峰值检测
  • SQL编程题复习(24/9/20)
  • VM虚拟机使用的镜像文件下载
  • Linux:虚拟文件系统/proc和self进程
  • [Unity Demo]从零开始制作空洞骑士Hollow Knight第六集:制作小骑士完整的跳跃落地行为
  • 力扣(leetcode)每日一题 LCR 187 破冰游戏(还是考的约瑟夫环)
  • F28335中断系统
  • 第二十节:学习Redis缓存数据库实现增删改查(自学Spring boot 3.x的第五天)
  • Linux线程同步—竞态条件与互斥锁、读写锁(C语言)
  • PLC通信协议的转化
  • xhs 小红书 x-s web 分析
  • 如何下载ComfyUI开发版