thread_local在C++ 11中意味着什么?

pol*_*pts 109 c++ multithreading thread-local thread-local-storage c++11

我对thread_localC++ 11中的描述感到困惑.我的理解是,每个线程在函数中都有唯一的局部变量副本.所有线程都可以访问全局/静态变量(可能使用锁进行同步访问).和thread_local变量的所有线程都是可见的,但只能由他们为其定义线程修改?这是对的吗?

pax*_*blo 130

线程本地存储持续时间是一个术语,用于表示看似全局或静态存储持续时间的数据(从使用它的函数的角度来看),但实际上,每个线程有一个副本.

它添加到当前自动(在块/函数期间存在),静态(存在于程序持续时间中)和动态(在分配和释放之间存在于堆上).

线程创建时会产生一些线程本地的东西,并在线程停止时被处理掉.

一些例子如下.

考虑一个随机数生成器,其中种子必须基于每个线程进行维护.使用线程本地种子意味着每个线程都有自己的随机数序列,与其他线程无关.

如果您的种子是随机函数中的局部变量,则每次调用它时都会初始化它,每次都给出相同的数字.如果它是全局的,线程会干扰彼此的序列.

另一个例子是strtok令牌化状态存储在特定于线程的基础上.这样,单个线程可以确保其他线程不会搞砸其令牌化工作,同时仍能够通过多次调用维持状态strtok- 这基本上呈现strtok_r(线程安全版本)冗余.

这两个示例都允许线程局部变量存在使用它的函数中.在预先线程化的代码中,它只是函数中的静态存储持续时间变量.对于线程,修改为线程本地存储持续时间.

又一个例子就是这样的errno.您不希望errno在其中一个调用失败之后但在检查变量之前修改单独的线程,但是每个线程只需要一个副本.

该站点具有不同存储持续时间说明符的合理描述.

  • 对不起,让我改一下.它没有引入strtok的任何_new_问题:-) (10认同)
  • 实际上,`r`代表"重入",这与线程安全无关.确实,您可以使用线程本地存储使某些事情在线程安全地工作,但是您无法使它们重新进入. (7认同)
  • 使用本地线程并不能解决`strtok`的问题.即使在单线程环境中,`strtok`也会被破坏. (4认同)
  • 在单线程环境中,只有当函数是调用图中循环的一部分时,才需要重入函数.叶子函数(不调用其他函数的函数)根据定义不是循环的一部分,并且没有充分理由为什么`strtok`应该调用其他函数. (3认同)
  • 这会弄乱它:`while(something){char*next = strtok(whatever); someFunction(下); // someFunction调用strtok}` (3认同)
  • ```thread_local 对象``` 是否在线程结束时调用它的解除分配器? (2认同)

Ant*_*ams 119

声明变量时,thread_local每个线程都有自己的副本.当您按名称引用它时,将使用与当前线程关联的副本.例如

thread_local int i=0;

void f(int newval){
    i=newval;
}

void g(){
    std::cout<<i;
}

void threadfunc(int id){
    f(id);
    ++i;
    g();
}

int main(){
    i=9;
    std::thread t1(threadfunc,1);
    std::thread t2(threadfunc,2);
    std::thread t3(threadfunc,3);

    t1.join();
    t2.join();
    t3.join();
    std::cout<<i<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)

此代码将输出"2349","3249","4239","4329","2439"或"3429",但绝不会输出任何其他内容.每个线程都有自己的副本i,分配给,递增然后打印.运行的线程main也有自己的副本,该副本在开头分配,然后保持不变.这些副本完全独立,每个副本都有不同的地址.

它只是在这方面特殊的名称 - 如果您获取thread_local变量的地址,那么您只需要一个指向普通对象的普通指针,您可以在线程之间自由传递.例如

thread_local int i=0;

void thread_func(int*p){
    *p=42;
}

int main(){
    i=9;
    std::thread t(thread_func,&i);
    t.join();
    std::cout<<i<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)

由于地址i被传递给线程函数,i因此可以分配属于主线程的副本,即使它是thread_local.因此该程序将输出"42".如果你这样做,那么你需要注意*p它所属的线程退出后没有被访问,否则你会得到一个悬空指针和未定义的行为,就像指向对象被销毁的任何其他情况一样.

thread_local变量在"首次使用之前"初始化,因此如果它们从未被给定线程触及,则它们不一定被初始化.这是为了允许编译器避免thread_local为程序中的每个变量构建一个完全独立且不接触其中任何一个的线程.例如

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class unused;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}
Run Code Online (Sandbox Code Playgroud)

在这个程序中有2个线程:主线程和手动创建的线程.两个线程都没有调用f,因此thread_local永远不会使用该对象.因此,未指定编译器是否将构造0,1或2个实例my_class,并且输出可以是"","hellohellogoodbyegoodbye"或"hellogoodbye".

  • 我认为重要的是要注意变量的线程本地副本是变量的新初始化副本。也就是说,如果你在 `threadFunc` 的开头添加一个 `g()` 调用,那么输出将是 `0304029` 或 `02`、`03` 和 `04` 对的其他排列。也就是说,即使在创建线程之前将 9 分配给了 `i`,线程也会获得一个新构造的 `i` 副本,其中 `i=0`。如果`i` 被分配了`thread_local int i = random_integer()`,那么每个线程都会得到一个新的随机整数。 (2认同)
  • 不完全是“02”、“03”、“04”的排列,可能还有其他序列,如“020043” (2认同)
  • 我刚刚发现的有趣的花絮:GCC 支持使用 thread_local 变量的地址作为模板参数,但其他编译器不支持(截至撰写本文时;尝试过 clang、vstudio)。我不确定标准对此有什么规定,或者这是否是一个未指定的区域。 (2认同)

Ker*_* SB 20

线程局部存储在每个方面都像静态(=全局)存储,只有每个线程都有一个单独的对象副本.对象的生命周期从线程开始(对于全局变量)或在第一次初始化(对于块局部静态)开始,并在线程结束时(即被join()调用时)结束.

因此,只有可以声明的变量才可以static声明为thread_local,即全局变量(更确切地说:命名空间范围内的变量"),静态类成员和块静态变量(在这种情况下static暗示).

例如,假设您有一个线程池,并想知道您的工作负载是如何平衡的:

thread_local Counter c;

void do_work()
{
    c.increment();
    // ...
}

int main()
{
    std::thread t(do_work);   // your thread-pool would go here
    t.join();
}
Run Code Online (Sandbox Code Playgroud)

这将打印线程使用情况统计信息,例如使用如下实现:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
Run Code Online (Sandbox Code Playgroud)