thread_local的一些用法

thread_local变量

thread_local变量是C++ 11新引入的一种存储类型。它会影响变量的存储周期(Storage duration),C++中有4种存储周期:

  1. automatic
  2. static
  3. dynamic
  4. thread

有且只有thread_local关键字修饰的变量具有线程周期(thread duration),这些变量(或者说对象)在线程开始的时候被生成(allocated),在线程结束的时候被销毁(deallocated)。并且每 一个线程都拥有一个独立的变量实例(Each thread has its own instance of the object)。thread_local 可以和static 与 extern关键字联合使用,这将影响变量的链接属性(to adjust linkage)。

 

那么,哪些变量可以被声明为thread_local?以下3类都是ok的

  1. 命名空间下的全局变量
  2. 类的static成员变量
  3. 本地变量
下面引用《C++ Concurrency in Action》书中的例子来说明这3种情况:
复制代码
thread_local int x;  //A thread-local variable at namespace scope
class X
{
    static thread_local std::string s; //A thread-local static class data member
};
static thread_local std::string X::s;  //The definition of X::s is required

void foo()
{
    thread_local std::vector<int> v;  //A thread-local local variable
}
复制代码

 

 

既然每个线程都拥有一份独立的thread_local变量,那么就有2个问题需要考虑:

  1. 各线程的thread_local变量是如何初始化的
  2. 各线程的thread_local变量在初始化之后拥有怎样的生命周期,特别是被声明为thread_local的本地变量(local variables)

下面的代码可以帮助回答这2个问题,我的测试环境是vs2015。

输出的前3行打印能帮助解答thread_local变量是如何初始化的,可以看到每个线程都会进行一次初始化,例子中的g_n在主线程中最早被初始化为1,随后被修改为2和3,但这些修改操作并不影响g_n在线程t2和t3中的初始值(值为1),虽然t2和t3线程启动的时候主线程中的变量值已经被更新为3,所以主线程、thread1、thread2打印结果分别为3,2,2。

后6行打印说明了一个事实,声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,它具有static变量一样的初始化特征和生命周期,虽然它并没有被声明为static。例子中foo函数中的thread_local变量 i 在每个线程第一次执行到的时候初始化,在每个线程各自累加,在线程结束时释放。

 

复制代码
#include <thread>

thread_local int g_n = 1;

void f()
{
    g_n++;
    printf("id=%d, n=%d\n", std::this_thread::get_id(),g_n);
}

void foo()
{
    thread_local int i=0;
    printf("id=%d, n=%d\n", std::this_thread::get_id(), i);
    i++;
}

void f2()
{
    foo();
    foo();
}

int main()
{
    g_n++; 
    f();    
    std::thread t1(f);
    std::thread t2(f);
    
    t1.join();
    t2.join();


    f2();
    std::thread t4(f2);
    std::thread t5(f2);

    t4.join();
    t5.join();
    return 0;
}
复制代码

 

输出(id值是每次运行时变的):

id=8004, n=3

id=8008, n=2
id=8012, n=2
id=8004, n=0
id=8004, n=1
id=8016, n=0
id=8016, n=1
id=8020, n=0
id=8020, n=1
 ==========================================================================================================
 

thread_local简介

thread_local是一个存储器指定符

所谓存储器指定符,其作用类似命名空间,指定了变量名的存储期以及链接方式。同类型的关键字还有

  • auto:自动存储期(C++11 前)。C++11 起,auto 不再是存储类说明符;它被用于指示类型推导。
  • register:自动存储期,指示编译器将此变量置于寄存器中。此关键词已于 C++17 被弃用。
  • static:静态或者线程存储期,同时指示是内部链接
  • extern:静态或线程存储期,同时提示是外部链接;
  • thread_local:线程存储期;
  • mutable:不影响存储期或链接

对于thread_local,官方解释是:

thread_local 只对声明于命名空间作用域的对象、声明于块作用域的对象以及静态数据成员允许。它指示对象拥有线程存储期。它能与 static 或 extern 结合,以分别指定内部或外部链接(除了静态数据成员始终拥有外部链接),但附加的 static 不影响存储期。

线程存储期:对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 一同出现,以调整链接。

实践

全局变量

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印, 加锁指示为了方便多线程打印

thread_local int x = 1;

void thread_func(const std::string&thread_name){
    for(int i = 0; i < 50; i++){
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出

在这里插入图片描述

总结:线程们会相互抢锁,但是每个线程都会在线程初始化时拷贝一个自己的x副本,互不影响
在这里插入图片描述

局部变量

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local  int x = 1;  //只在每个线程创建时初始化一次
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出: 只在每个线程创建时初始化一次

thread[t1]: x = 2
thread[t1]: x = 3
thread[t1]: x = 4
thread[t2]: x = 2
thread[t2]: x = 3
thread[t2]: x = 4

如果不加thread_local,相当于局部变量

thread[t2]: x = 2
thread[t2]: x = 2
thread[t2]: x = 2
thread[t1]: x = 2
thread[t1]: x = 2
thread[t1]: x = 2

如果改成static int x = 1;,每次输出的结果都是不相同的,是线程不安全的

thread[t2]: x = 3
thread[t2]: x = 4
thread[t2]: x = 5
thread[t1]: x = 5
thread[t1]: x = 6
thread[t1]: x = 7

这里还有一个要注意的地方,就是** thread_local 虽然改变了变量的存储周期,但是并没有改变变量的使用周期或者说作用域**,比如上述的局部变量,其使用范围不能超过 for 循环外部,否则编译出错。(static也没有改变作用域,只是改变存储周期)

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local int x = 1; // static int x = 1;
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
    x++;    //编译会出错:error: ‘x’ was not declared in this scope
    return;
}

类对象

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印

class A{
public:
    A(){
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "create A" << std::endl;
    }

    ~A() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "destroy A" << std::endl;
    }

    int counter = 0;
    int get_value() {
        return counter++;
    }
};

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local A* a = new A();
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出:

create A
thread[t1]: a.counter:0
thread[t1]: a.counter:1
thread[t1]: a.counter:2
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:1
thread[t2]: a.counter:2

可以看出类对象的使用和创建和局部变量类似,都不会创建多个。这种情况在函数间或通过函数返回实例也是一样的:

A* creatA() {
    return new A();
}

void loopin_func(const std::string& thread_name) {
    thread_local A* a = creatA();
    std::lock_guard<std::mutex> lock(cout_mutex);
    std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    return;
}

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {    
        loopin_func(thread_name);
    }
    return;
}

输出:

create A
thread[t1]: a.counter:0
thread[t1]: a.counter:1
thread[t1]: a.counter:2
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:1
thread[t2]: a.counter:2

虽然 createA() 看上去被调用了多次,实际上只被调用了一次,因为thread_local 变量只会在每个线程最开始被调用的时候进行初始化,并且只会被初始化一次。

举一反三,如果不是初始化,而是赋值,则情况就不同了:

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local A*  a;
        a = new A();
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出

create A
thread[t1]: a.counter:0
create A
thread[t1]: a.counter:0
create A
thread[t1]: a.counter:0
create A
thread[t2]: a.counter:0
create A
thread[t2]: a.counter:0
create A
thread[t2]: a.counter:0



很明显,虽然只初始化一次,但却可以被多次赋值,因此 C++ 变量初始化是十分重要的

类成员变量

规定:thread_local 作为类成员变量时必须是 static 的。

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印


class B {
public:
    B() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "create B" << std::endl;
    }
    ~B() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "destroy B" << std::endl;
    }
    thread_local static int b_key;
    //thread_local int b_key;
    int b_value = 24;
    static int b_static;
};
thread_local int B::b_key = 12;
int B::b_static = 36;
void thread_func(const std::string& thread_name) {
    B b;
    for (int i = 0; i < 3; ++i) {
        b.b_key--;
        b.b_value--;
        B::b_static--;   // not thread safe
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: b_key:" << b.b_key << ", b_value:" << b.b_value << ", b_static:" << b.b_static << std::endl;
        std::cout << "thread[" << thread_name << "]: B::key:" << B::b_key << ", b_value:" << b.b_value << ", b_static: " << B::b_static << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出:

create B
thread[t2]: b_key:11, b_value:23, b_static:35
thread[t2]: B::key:11, b_value:23, b_static: 35
thread[t2]: b_key:10, b_value:22, b_static:34
thread[t2]: B::key:10, b_value:22, b_static: 34
thread[t2]: b_key:9, b_value:21, b_static:33
thread[t2]: B::key:9, b_value:21, b_static: 33
destroy B
create B
thread[t1]: b_key:11, b_value:23, b_static:32
thread[t1]: B::key:11, b_value:23, b_static: 32
thread[t1]: b_key:10, b_value:22, b_static:31
thread[t1]: B::key:10, b_value:22, b_static: 31
thread[t1]: b_key:9, b_value:21, b_static:30
thread[t1]: B::key:9, b_value:21, b_static: 30
destroy B

Process finished with exit code 0

b_key 是 thread_local,虽然其也是 static 的,但是每个线程中有一个,每次线程中的所有调用共享这个变量。b_static 是真正的 static,全局只有一个,所有线程共享这个变量。

 
===========================================================================================================

C++Thread Local Storage

概述
thread_local指示对象拥有线程存储期。也就是对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 结合一同出现,以调整链接(分别指定内部或外部链接),详细的可以查阅:存储类说明符 - cppreference.com。

使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。
thread_local 说明符可以与 static 或 extern 合并。这将影响变量的链接属性。

表现形式
在 C++ 中,Thread Local Storage (TLS) 是通过使用 thread_local 关键字来实现的。它可以用于修饰全局变量、静态变量或类的静态成员变量,使其成为线程局部变量。对于一个使用 thread_local 修饰的变量,每个线程都拥有一个独立的副本。以下是使用 thread_local 的一些原型示例:

全局变量
每个线程都有自己单独的x副本,互不干预。
thread_local int globalVar;
1
局部变量(静态变量)
线程存储期的变量都是和线程绑定的,所以只有第一次声明时被赋值。可以理解为线程专用的static变量。不过变量的作用域依然是在本身的作用域内。
void foo() {
thread_local int staticVar;
}


类对象(静态变量)
与局部变量的情况相同,创建的实例相对于thread是static的,一般情况要求我们:thread_local对象声明时赋值.
类成员变量

thread_local作为类成员变量时必须是static的.
thread_local作为类成员时也是对于每个thread分别分配了一个,而static则是全局一个.
class MyClass {
public:
static thread_local int staticMemberVar;
};

需要注意的是,使用 thread_local 关键字修饰的变量在每个线程中都有一个独立的副本,因此每个线程对其进行的操作都不会影响其他线程。在多线程环境下,这可以帮助避免数据竞争和同步开销。

作用和适用的场景
C++ 的 Thread Local Storage(TLS)特性在多线程环境中非常有用,它允许每个线程拥有一份单独的全局或静态变量副本。适用的场景包括:

避免数据竞争:当多个线程需要访问全局或静态变量时,使用 TLS 可以为每个线程提供一个变量副本,从而避免数据竞争。每个线程只操作自己的变量副本,不会影响其他线程。
减少同步开销:由于每个线程都有自己的变量副本,因此在某些情况下可以减少同步开销,例如减少互斥锁的使用。这可以提高多线程程序的性能。
线程独立的资源分配:当某个资源需要在多个线程之间共享,但又希望每个线程独立使用该资源时,可以使用 TLS。例如,线程独立的内存分配器、线程独立的随机数生成器等。
线程特有的状态跟踪:TLS 可用于跟踪每个线程的特定状态,如错误信息、递归调用计数、线程局部缓存等。这可以使每个线程在处理自己的数据时,能够访问和操作这些特定状态。
模拟线程局部变量:在一些没有原生支持线程局部变量的环境中,可以使用 TLS 来模拟这个功能。例如,在 C++11 之前的版本中,可以通过自定义的实现来模拟线程局部变量。
需要注意的是,TLS 可能会增加内存消耗,因为每个线程都有自己的变量副本。此外,在某些情况下,TLS 可能会导致一定的性能损失,因为操作系统需要在线程切换时处理线程局部数据。因此,在使用 TLS 时,请确保权衡这些因素,并在适当的场景下使用。

实现逻辑
C++ 的 Thread Local Storage(TLS)特性在底层的实现和逻辑上有很多不同的方面。主要实现方法包括编译器支持、操作系统支持和库支持。在这里,我们将概述 TLS 的一般实现原理和底层逻辑:

编译器支持:编译器在生成目标代码时,对于使用 thread_local 修饰的变量,会将其分配到特殊的存储区域。这个存储区域是每个线程独立的,因此在运行时,每个线程都可以访问自己独立的变量副本。
操作系统支持:操作系统通常提供一种机制来管理线程局部存储。例如,Windows 使用 TlsAlloc、TlsGetValue 和 TlsSetValue 等函数管理线程局部存储,而 Linux 使用 pthread_key_create、pthread_getspecific 和 pthread_setspecific 等函数。当一个线程访问 thread_local 变量时,操作系统会将请求重定向到这个线程对应的局部存储区域。
库支持:在 C++ 标准库中,线程局部存储的功能由 thread_local 关键字和相关的库函数提供。这些库函数在底层调用操作系统提供的 API 来实现线程局部存储的功能。
线程创建时的初始化:当一个新线程被创建时,运行时系统会分配一个独立的 TLS 区域,并为该区域中的 thread_local 变量进行初始化。这些初始化操作可能包括调用默认构造函数、执行初始化表达式等。
线程退出时的清理:当一个线程退出时,运行时系统会负责释放线程局部存储区域,并为该区域中的 thread_local 变量调用析构函数。这个过程通常由操作系统或 C++ 运行时库自动完成。
需要注意的是,不同的编译器和操作系统可能有不同的实现细节。在实际使用中,开发者无需了解所有这些底层逻辑,只需使用 thread_local 关键字和相关的库函数就可以享受到线程局部存储带来的便利。
示例
示例1:使用 thread_local 计算每个线程的累积和
#include <iostream>
#include <thread>
#include <vector>

thread_local int sum = 0;

void accumulate(int n) {
for (int i = 1; i <= n; ++i) {
sum += i;
}
std::cout << "Thread id: " << std::this_thread::get_id() << ", Sum: " << sum << std::endl;
}

int main() {
std::vector<std::thread> threads;

for (int i = 1; i <= 5; ++i) {
threads.emplace_back(accumulate, i * 10);
}

for (auto& t : threads) {
t.join();
}

return 0;
}

示例2:模拟一个简单的线程安全计数器


#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

std::mutex mtx;
unsigned int globalCounter = 0;
thread_local unsigned int localCounter = 0;

void incrementCounter(int n) {
for (int i = 0; i < n; ++i) {
++localCounter;
std::unique_lock<std::mutex> lock(mtx);
++globalCounter;
lock.unlock();
}

std::cout << "Thread id: " << std::this_thread::get_id()
<< ", Local counter: " << localCounter
<< ", Global counter: " << globalCounter << std::endl;
}

int main() {
std::vector<std::thread> threads;

for (int i = 0; i < 5; ++i) {
threads.emplace_back(incrementCounter, 100);
}

for (auto& t : threads) {
t.join();
}

std::cout << "Final global counter: " << globalCounter << std::endl;

return 0;
}

这两个示例展示了如何使用 thread_local 关键字创建线程局部变量。在第一个示例中,我们计算了每个线程的累积和。在第二个示例中,我们模拟了一个简单的线程安全计数器,它使用 thread_local 存储每个线程的局部计数,并使用互斥锁保护全局计数器。

需要注意的地方
thread_local 变量是线程局部存储的变量,它的生命周期与线程的生命周期相同。在多线程程序中,thread_local 变量可以用来保存线程相关的状态,从而避免了线程间的共享状态可能导致的同步问题。
然而,如果在使用 thread_local 变量时不小心,可能会导致内存泄漏问题。具体来说,如果一个线程结束时,它仍然持有一个指向 thread_local 变量的指针,那么这个 thread_local 变量就会一直存在,直到整个程序结束才会被释放。如果这个 thread_local 变量是一个大的对象,那么就会导致内存泄漏。
为了避免这个问题,需要确保在线程结束时及时清理 thread_local 变量。一种常见的做法是在 thread_local 变量的析构函数中清理相关的资源。例如,如果 thread_local 变量是一个指针类型,那么在析构函数中应该释放指针所指向的资源。同时,还可以使用一些 RAII 技巧,例如使用 std::unique_ptr 等智能指针,来帮助自动化资源清理的工作。

除了在 thread_local 变量的析构函数中及时清理资源外,还有一些其他的可能导致内存泄漏的情况,例如:
1.在使用 thread_local 变量时发生异常,导致析构函数没有被正确调用,从而导致资源泄漏。
2.在使用 thread_local 变量的线程中调用 std::exit 函数或者使用 std::quick_exit 函数退出程序,导致 thread_local 变量的析构函数没有被调用,从而导致资源泄漏。
3.在使用 thread_local 变量的线程中创建了一些子线程,并且这些子线程也使用了相同的 thread_local 变量,但是没有正确清理子线程中的 thread_local 变量,从而导致资源泄漏。

针对这些情况,我们需要采取相应的措施来避免内存泄漏,例如:
1.使用 RAII 技巧,例如使用 std::unique_ptr 等智能指针来管理资源,从而避免异常导致的资源泄漏。
2.在程序退出时,可以使用 std::atexit 函数来注册一个清理函数,该函数会在程序退出前被调用,从而确保 thread_local 变量的析构函数被正确调用。
3.在子线程中正确清理 thread_local 变量,可以使用类似于在主线程中清理 thread_local 变量的方式,即在 thread_local 变量的析构函数中清理相关资源。同时,还需要确保在子线程退出时,thread_local 变量的析构函数被正确调用。

总结
本质上thread_local修饰后仍然是一个变量,我们依旧能够使用取地址操作者通过引用的方法传递给其他线程对其进行修改,
thread-local storage 和 static(或者说global) 存储很类似,每一个线程都将拥有一份这个数据的拷贝,thread_local对象的生命周期从线程开始时开始(对于全局变量),或者首先分配空间。当线程退出的时候对象析构;
一般在声明时赋值,在本thread中只执行一次。当用于类成员变量时,必须是static的。
参考:https://blog.csdn.net/qq_21438461/article/details/129408082

posted @ 2024-02-21 17:17  白伟碧一些小心得  阅读(422)  评论(0编辑  收藏  举报