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

时间:2012-08-16 09:05:37

标签: c++ multithreading c++11 thread-local thread-local-storage

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

3 个答案:

答案 0 :(得分:109)

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

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

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

以下是一些例子。

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

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

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

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

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

This site对不同的存储持续时间说明符有合理的描述。

答案 1 :(得分:103)

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

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;
}

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

只有名称在这方面很特别 - 如果你取一个thread_local变量的地址,那么你只需要一个指向普通对象的普通指针,你可以在线程之间自由传递。 e.g。

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;
}

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

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

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

void f(){
    thread_local my_class;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}

在这个程序中有2个线程:主线程和手动创建的线程。两个线程都没有调用f,因此永远不会使用thread_local对象。因此,未指定编译器是构造{1}}的0,1或2个实例,输出可能是“”,“hellohellogoodbyegoodbye”或“hellogoodbye”。

答案 2 :(得分:19)

线程局部存储在每个方面都像静态(=全局)存储,只有每个线程都有一个单独的对象副本。对象的生命周期从线程开始(对于全局变量)或在第一次初始化(对于块局部静态)开始,并在线程结束时(即调用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();
}

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

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