为什么thread_local不能应用于非静态数据成员,以及如何实现线程本地非静态数据成员

Why may thread_local not be applied to non-static data members and how to implement thread-local non-static data members?

本文关键字:静态 数据成员 何实现 实现 线程 local thread 不能 应用于 为什么      更新时间:2023-10-16

为什么thread_local不能应用于非静态数据成员?对于这个问题,公认的答案是:"将非静态结构或类成员设置为线程局部是没有意义的。"老实说,我看到了很多让非静态数据成员线程局部化的好理由。

假设我们有某种ComputeEngine,其成员函数computeSomething被连续调用多次。成员函数内部的一些工作可以并行完成。为此,每个线程都需要某种ComputeHelper来提供,例如,辅助数据结构。所以我们实际需要的是:

class ComputeEngine {
 public:
  int computeSomething(Args args) {
    int sum = 0;
    #pragma omp parallel for reduction(+:sum)
    for (int i = 0; i < MAX; ++i) {
      // ...
      helper.xxx();
      // ...
    }
    return sum;
  }
 private:
  thread_local ComputeHelper helper;
};

不幸的是,这段代码无法编译。我们可以这样做:

class ComputeEngine {
 public:
  int computeSomething(Args args) {
    int sum = 0;
    #pragma omp parallel
    {
      ComputeHelper helper;
      #pragma omp for reduction(+:sum)
      for (int i = 0; i < MAX; ++i) {
        // ...
        helper.xxx();
        // ...
      }
    }
    return sum;
  }
};

然而,这将在computeSomething的连续调用之间构造和销毁ComputeHelper。假设构造ComputeHelper是昂贵的(例如,由于巨大向量的分配和初始化),我们可能希望在连续调用之间重用ComputeHelper。这让我想到了下面的模板方法:

class ComputeEngine {
  struct ThreadLocalStorage {
    ComputeHelper helper;
  };
 public:
  int computeSomething(Args args) {
    int sum = 0;
    #pragma omp parallel
    {
      ComputeHelper &helper = tls[omp_get_thread_num()].helper;
      #pragma omp for reduction(+:sum)
      for (int i = 0; i < MAX; ++i) {
        // ...
        helper.xxx();
        // ...
      }
    }
    return sum;
  }
 private:
  std::vector<ThreadLocalStorage> tls;
};
  1. 为什么thread_local不能用于非静态数据成员?什么这一限制背后的理由是什么?难道我没有给你一个好机会吗在这个例子中,线程本地的非静态数据成员非常完美理解吗?
  2. 实现线程局部非静态的最佳实践是什么数据成员吗?

至于为什么thread_local不能应用于非静态数据成员,它会破坏这些成员通常的排序保证。也就是说,单个public/private/protected组中的数据成员必须按照与类声明相同的顺序在内存中布局。更不用说如果你在堆栈上分配一个类会发生什么——TLS成员不会进入堆栈。

至于如何解决这个问题,我建议使用boost::thread_specific_ptr。你可以把其中一个放到你的类中,得到你想要的行为。

线程本地存储的工作方式通常是在线程特定的数据结构(例如Windows中的TEB)中获得一个指针

只要所有线程局部变量都是静态的,编译器就可以很容易地计算出这些字段的大小,分配相应大小的结构体,并在该结构体中为每个字段分配一个静态偏移量。

一旦允许非静态字段,整个方案就会变得更加复杂——解决这个问题的一种方法是在每个类中增加一个间接层并存储一个索引(现在你在类中有了隐藏字段,这是意想不到的)。

他们显然决定让每个应用程序根据需要来处理它,而不是将这种方案的复杂性提升到实现者身上。