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

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

为什么 thread_local 不能应用于非静态数据成员? this question 的公认答案说:"There is no point in making non-static structure or class members 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 组中的数据成员必须按照与 class 声明中相同的顺序排列在内存中。更不用说如果在堆栈上分配 class 会发生什么——TLS 成员不会进入堆栈。

至于如何解决这个问题,我建议使用 boost::thread_specific_ptr。您可以将其中之一放入 class 并获得您想要的行为。

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

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

一旦你允许非静态字段,整个方案就会变得更加复杂 - 解决它的一种方法是增加一个间接级别并在每个 class 中存储一个索引(现在你有隐藏字段在 classes 中,相当出乎意料)。

他们显然决定让每个应用程序根据需要处理它,而不是将这种方案的复杂性强加给实施者。