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

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

相关问题:https://dev59.com/JGEh5IYBdhLWcg3wHARD请返回已翻译的文本。 - Hari
3个回答

224

线程本地存储期(Thread-local storage duration)是一个词语,用来指代数据在函数使用时看起来像全局或静态存储期(从视角上看),但实际上每个线程都有一份副本。

它增加了以下当前选项:

  • 自动变量(存在于块或函数期间);
  • 静态变量(程序持续时间内存在);以及
  • 动态变量(在分配和释放之间在堆中存在)。

线程本地的东西在线程创建时被引入并在线程结束时被处理。


例如,假设有一个需要基于每个线程保持种子的随机数生成器。使用线程本地种子意味着每个线程都有自己的随机数序列,独立于所有其他线程。

如果您的种子是随机函数内的本地变量,则每次调用它都会初始化,给出相同的结果。如果它是全局的,则不同线程将互相干扰。


另一个例子是像 strtok 这样的东西,其中分词状态存储在基于线程的基础上。这样,单个线程可以确保其他线程不会破坏其分词努力,同时仍能够在多次调用 strtok 时保持状态 - 这基本上使 strtok_r(线程安全版本)变得多余。


另一个例子是像 errno 这样的东西。在您检查结果之前,您不希望单独的线程修改 errno,但在其中一个调用失败之后。


此站点有关于不同存储期限定符的合理描述。


6
使用线程局部变量并不能解决strtok存在的问题。即使在单线程环境中,strtok仍然存在问题。 - James Kanze
20
抱歉,让我重新说一遍。这不会引入任何与 strtok 相关的新问题 :-) - paxdiablo
15
实际上,r代表“可重入”,与线程安全无关。尽管使用线程本地存储可以使某些内容能够在多线程中安全运行,但它们并不能做到可重入。 - Kerrek SB
7
在单线程环境中,仅当函数是调用图中的一个循环部分时,函数需要是可重入的。叶子函数(不调用其他函数的函数)根据定义不是循环的一部分,并且没有充分的理由说明 strtok 应该调用其他函数。 - MSalters
6
这会弄乱它: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction 调用了 strtok } - japreiss
显示剩余4条评论

184

当你声明一个变量为 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 变量的线程构造程序中的每个 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();
}

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

6
我认为需要注意的是,线程本地变量的副本是一个新初始化的变量副本。也就是说,如果在threadFunc的开头添加g()调用,则输出将为0304029或其他对020304进行排列组合的结果。即使在创建线程之前将9分配给i,线程也会获得一个新构造的i副本,其中i=0。如果使用thread_local int i = random_integer()分配i,则每个线程都会获得一个新的随机整数。 - Mark H
2
不完全是 020304 的排列,可能还有其他序列,比如 020043 - Hongxu Chen
2
有趣的小细节:我刚发现GCC支持将thread_local变量的地址用作模板参数,但其他编译器不支持(截至本文撰写时;已尝试clang、vstudio)。我不确定标准对此有何规定,或者这是否是一个未指定的领域。 - jwd

31

线程本地存储在各个方面都像静态(=全局)存储,只是每个线程有一个对象的独立副本。该对象的生命周期要么始于线程启动(对于全局变量),要么始于首次初始化(对于块本地静态变量),并在线程结束时结束(即调用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;
     }
};

你是不是在你的std::cout中打错了,本意是std::this_thread::get_id() - Franky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接