Thread Local 模式介绍

使用的目的

        保护全局变量到一个线程,防止被其他线程破坏。如果你在Callable对象或Runnable对象中使用不是只读的类变量或静态变量时,必需使用ThreadLocal。
        通过应用Thread Local,你可以在整个处理请求过程中跟踪应用程序实例或本地设置。在Java中,ThreadLocal类像静态变量一样工作,区别在于它只能绑定到当前线程!使得我们可以8以线程安全的方式使用静态变量。 

现实世界的例子

在Java中,线程局部变量由ThreadLocal类对象实现。该类提供线程局部变量。

ThreadLocal保存T类型的变量,可通过get / set方法访问。例如,保存Integer值的ThreadLocal变量如下所示:

private static final ThreadLocal<Integer> myThreadLocalInteger = new ThreadLocal<Integer>();

源代码

Java种ThreadLocal类提供线程局部变量。这些变量与它们副本的不同之处在于,访问一个变量的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是一个私有静态的作为类中与线程相关联的字段。(如user ID oh 或者Transaction ID)  

通过应用ThreadLocal模式,你可以在整个处理请求过程中跟踪应用程序实例或字段设置。除了它只能绑定到当前线程,ThreadLocal类可以像静态变量一样工作,这样就允许我们以线程安全的方式使用静态变量。在Java中,线程局部变量由ThreadLocal类对象实现。ThreadLocal包含一个T类型的变量,可以通过get / set方法访问它。
例如:SimpleDateFormat是基本的Java类之一,并且不是线程安全的。如果你没有为每个线程分离SimpleDateFormat的实例,就会出现问题。 
在下面这个例子中,SimpleDateFormat的使用被设置为线程安全的。这是ThreadLocal模式的一个很明显的例子。
类图


第1步:创建DateFormatCallable类,它使用SimpleDateFormat将字符串日期转换为日期格式。日期格式和日期值将由构造函数传递给Callable。构造函数创建一个SimpleDateFormat实例并将其存储在一个ThreadLocal类变量中

public class DateFormatCallable implements Callable<Result> {
  // class variables (members)
  private ThreadLocal<DateFormat> df;    //TLTL   
  // private DateFormat df;                 //NTLNTL

  private String dateValue; // for dateValue Thread Local not needed
  

  /**
   * The date format and the date value are passed to the constructor
   * 
   * @param inDateFormat
   *          string date format string, e.g. "dd/MM/yyyy"
   * @param inDateValue
   *          string date value, e.g. "21/06/2016"
   */
  public DateFormatCallable(String inDateFormat, String inDateValue) {
    final String idf = inDateFormat;                 //TLTL
    this.df = new ThreadLocal<DateFormat>() {        //TLTL
      @Override                                      //TLTL
      protected DateFormat initialValue() {          //TLTL
        return new SimpleDateFormat(idf);            //TLTL
      }                                              //TLTL
    };                                               //TLTL
    // this.df = new SimpleDateFormat(inDateFormat);    //NTLNTL
    this.dateValue = inDateValue;
  }

  /**
   * @see java.util.concurrent.Callable#call()
   */
  @Override
  public Result call() {
    System.out.println(Thread.currentThread() + " started executing...");
    Result result = new Result();

    // Convert date value to date 5 times
    for (int i = 1; i <= 5; i++) {
      try {
        // this is the statement where it is important to have the
        // instance of SimpleDateFormat locally
        // Create the date value and store it in dateList
        result.getDateList().add(this.df.get().parse(this.dateValue));   //TLTL
//      result.getDateList().add(this.df.parse(this.dateValue));           //NTLNTL
      } catch (Exception e) {
        // write the Exception to a list and continue work
        result.getExceptionList().add(e.getClass() + ": " + e.getMessage());
      }

    }

    System.out.println(Thread.currentThread() + " finished processing part of the thread");

    return result;
  }
}


第2步:创建将由Callable DateFormatCallable返回的Result对象。

public class Result {
  // A list to collect the date values created in one thread
  private List<Date> dateList = new ArrayList<Date>();

  // A list to collect Exceptions thrown in one threads (should be none in
  // this example)
  private List<String> exceptionList = new ArrayList<String>();
  
  /**
   * 
   * @return List of date values collected within an thread execution
   */
  public List<Date> getDateList() {
    return dateList;
  }

  /**
   * 
   * @return List of exceptions thrown within an thread execution
   */
  public List<String> getExceptionList() {
    return exceptionList;
  }
}
第3步:让我们测试这种设计模式。创建ThreadLocalStorageDemo类使用Java类SimpleDateFormat将字符串日期值15/12/2015转换为日期格式。它使用4个线程执行此操作20次,每次执行5次。通过在DateFormatCallable中使用ThreadLocal,一切运行良好。但是,如果你注释掉ThreadLocal变体(ThreadLocal variant,标记为“// TLTL”)并注释非ThreadLocal变体(标记为“// NTLNTL”),则可以看到没有ThreadLocal会发生什么。很可能你会得到不正确的日期值和/或例外。

public class ThreadLocalStorageDemo {
  /**
   * Program entry point
   * 
   * @param args
   *          command line args
   */
  public static void main(String[] args) {
    int counterDateValues = 0;
    int counterExceptions = 0;

    // Create a callable
    DateFormatCallable callableDf = new DateFormatCallable("dd/MM/yyyy", "15/12/2015");
    // start 4 threads, each using the same Callable instance
    ExecutorService executor = Executors.newCachedThreadPool();

    Future<Result> futureResult1 = executor.submit(callableDf);
    Future<Result> futureResult2 = executor.submit(callableDf);
    Future<Result> futureResult3 = executor.submit(callableDf);
    Future<Result> futureResult4 = executor.submit(callableDf);
    try {
      Result[] result = new Result[4];
      result[0] = futureResult1.get();
      result[1] = futureResult2.get();
      result[2] = futureResult3.get();
      result[3] = futureResult4.get();

      // Print results of thread executions (converted dates and raised exceptions)
      // and count them
      for (int i = 0; i < result.length; i++) {
        counterDateValues = counterDateValues + printAndCountDates(result[i]);
        counterExceptions = counterExceptions + printAndCountExceptions(result[i]);
      }

      // a correct run should deliver 20 times 15.12.2015
      // and a correct run shouldn't deliver any exception
      System.out.println("The List dateList contains " + counterDateValues + " date values");
      System.out.println("The List exceptionList contains " + counterExceptions + " exceptions");

    } catch (Exception e) {
      System.out.println("Abnormal end of program. Program throws exception: " + e); 
    }
    executor.shutdown();
  }

  /**
   * Print result (date values) of a thread execution and count dates
   * 
   * @param res  contains results of a thread execution
   */
  private static int printAndCountDates(Result res) {
    // a correct run should deliver 5 times 15.12.2015 per each thread
    int counter = 0;
    for (Date dt : res.getDateList()) {
      counter++;
      Calendar cal = Calendar.getInstance();
      cal.setTime(dt);
      // Formatted output of the date value: DD.MM.YYYY
      System.out.println(
          cal.get(Calendar.DAY_OF_MONTH) + "." + cal.get(Calendar.MONTH) + "." + +cal.get(Calendar.YEAR));
    }
    return counter;
  }

  /**
   * Print result (exceptions) of a thread execution and count exceptions
   * 
   * @param res  contains results of a thread execution
   * @return number of dates
   */
  private static int printAndCountExceptions(Result res) {
    // a correct run shouldn't deliver any exception
    int counter = 0;
    for (String ex : res.getExceptionList()) {
      counter++;
      System.out.println(ex);
    }
    return counter;
  }
}

适用性

在以下任何情况下使用Thread Local 
  • 当你在Callable / Runnable对象中使用不是只读的类变量,并且在多个并行运行的线程中使用相同的Callable实例时。
  • 当你在Callable / Runnable对象中使用不是只读的静态变量时,Callable / Runnable的多个实例可能会在并行线程中运行。

本文翻译自谷歌Java社群,由于本人水平有限,可能翻译的不够准确,如有错误欢迎指正!

原文链接:https://plus.google.com/u/0/103003859659574905974/posts/68GmtRDZBtg



<think>好的,我现在需要回答用户关于Linux下C++中线程本地存储(Thread Local Storage, TLS)原理的问题。用户之前已经询问过ThreadLocalJava中的实现,现在转向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、付费专栏及课程。

余额充值