在C ++ 11中thread_local是什么意思?


131

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

Answers:


151

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

它添加到当前的自动变量(存在于块/功能中),静态变量(存在于程序期间)和动态变量(存在于分配和释放之间的堆中)。

线程本地化的某些东西在线程创建时就存在,并在线程停止时被丢弃。

以下是一些示例。

想想一个随机数生成器,必须在每个线程的基础上维护种子。使用线程本地种子意味着每个线程都获得自己的随机数序列,而与其他线程无关。

如果您的种子是随机函数中的局部变量,则每次调用它时都会对其进行初始化,从而每次都给您相同的数字。如果是全局变量,线程将相互干扰。

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

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

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

该站点对不同的存储期限说明符有合理的描述。


4
使用本地线程无法解决的问题strtokstrtok即使在单线程环境中也会损坏。
詹姆斯·坎泽

11
对不起,让我改一下。它不会给strtok带来任何问题:-)
paxdiablo 2012年

7
实际上,r代表“可重入”,这与线程安全无关。的确,您可以使某些事情与线程本地存储一起安全地在线程中工作,但不能使它们重新进入。
Kerrek SB 2012年

5
在单线程环境中,仅当函数是调用图中的循环的一部分时,才需要重新进入函数。叶子函数(一个不调用其他函数的函数)根据定义不属于循环的一部分,因此没有充分的理由strtok应该调用其他函数。
MSalters

3
这会搞砸: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss

135

声明变量时,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;
}

此代码将输出“ 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;
}

由于的地址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();
}

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


1
我认为必须注意,变量的线程本地副本是变量的新初始化副本。也就是说,如果你添加一个g()呼叫的开始threadFunc,那么输出将0304029或对其它一些置换020304。也就是说,即使i在创建线程之前将9分配给了线程,这些线程也会获得iwhere 的新构造副本i=0。如果i为分配了thread_local int i = random_integer(),则每个线程将获得一个新的随机整数。
Mark H

不完全的置换020304,有可能像其他序列020043
虹许晨

我刚刚发现了有趣的花絮:GCC支持使用thread_local变量的地址作为模板参数,但其他编译器不支持(在撰写本文时;尝试过clang,vstudio)。我不确定标准对此要说些什么,或者不确定这是一个未指定的领域。
jwd

23

线程本地存储在各个方面都像静态(=全局)存储一样,只是每个线程都具有对象的单独副本。对象的生命周期从线程启动(对于全局变量)或首次初始化(对于块局部静态变量)开始,并在线程结束(即何时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;
     }
};
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.