在 C++ 多线程编程中,线程局部存储(Thread Local Storage)是一项非常重要且值得深入理解的技术。它在提升线程安全性、简化并发设计中发挥着关键作用,是每一位 C++ 开发者都应掌握的核心知识之一。
1. 问题场景
假设在多线程场景下,我们需要创建两个线程来执行 worker 任务,并在 worker 中需要多次调用 log 函数打印日志,我们希望每个线程打印的日志都有独立编号。例如,线程一打印的日志以 [线程一] 为标识,有 log#1、log#2 等编号;线程二打印的日志以 [线程二] 为标识,也有对应的 log#1、log#2 等编号 。

注意:在这个场景中,要求为每个线程分配一个具备线程私有、与线程生命周期同步的变量,专门用于记录该线程的日志编号。
#include <iostream>
#include <thread>
#include <mutex>
// 线程锁,用于同步变量和标准输出
std::mutex lock;
// 普通全局变量
int counter = 0;
// 静态局部变量
//static int counter = 0;
void log(const std::string& thread_name, const std::string& log_message)
{
// 静态局部变量
//static int counter = 0;
// 普通局部变量
int counter = 0;
std::lock_guard<std::mutex> guard(lock);
++counter;
std::cout << "[" << thread_name << "] log#" << counter << ": " << log_message << std::endl;
}
void worker(const std::string& thread_name)
{
log(thread_name, "数据开始处理");
log(thread_name, "数据正在处理");
log(thread_name, "数据处理结束");
}
void test()
{
std::thread t1(worker, "线程一");
std::thread t2(worker, "线程二");
t1.join();
t2.join();
}
int main()
{
test();
return 0;
}
在 C++ 中,对于 counter 变量,我们有四种定义方式:
- 普通局部变量:线程私有,无法做到与线程生命周期同步
- 静态局部变量:线程共享,无法做到与线程生命周期同步(生命周期是整个程序运行期间存在)
- 普通全局变量:线程共享,无法做到与线程生命周期同步(生命周期是整个程序运行期间存在)
- 静态全局变量:线程共享,无法做到与线程生命周期同步(生命周期是整个程序运行期间存在)
最终,我们发现这四种方式都无法满足多线程编程场景下,对 counter 变量的要求。程序输出结果:
[线程一] log#1: 数据开始处理 [线程一] log#2: 数据正在处理 [线程一] log#3: 数据处理结束 [线程二] log#4: 数据开始处理 [线程二] log#5: 数据正在处理 [线程二] log#6: 数据处理结束
为了解决这个问题,我们可以使用 C++11 中引入的 thread_local 关键字,它可以用于定义线程私有、与线程生命周期同步的变量。我们把程序修改如下:
#include <iostream>
#include <thread>
#include <mutex>
// 线程锁,用于同步变量和标准输出
std::mutex lock;
// 线程局部变量
thread_local int counter = 0;
void log(const std::string& thread_name, const std::string& log_message)
{
++counter;
std::lock_guard<std::mutex> guard(lock);
std::cout << thread_name << "地址: " << (long)&counter << std::endl;
std::cout << "[" << thread_name << "] log#" << counter << ": " << log_message << std::endl;
}
void worker(const std::string& thread_name)
{
log(thread_name, "数据开始处理");
log(thread_name, "数据正在处理");
log(thread_name, "数据处理结束");
}
void test()
{
std::thread t1(worker, "线程一");
std::thread t2(worker, "线程二");
t1.join();
t2.join();
}
int main()
{
test();
return 0;
}
程序执行结果:
线程二地址: 2816655625780 [线程二] log#1: 数据开始处理 线程二地址: 2816655625780 [线程二] log#2: 数据正在处理 线程二地址: 2816655625780 [线程二] log#3: 数据处理结束 线程一地址: 2816655644932 [线程一] log#1: 数据开始处理 线程一地址: 2816655644932 [线程一] log#2: 数据正在处理 线程一地址: 2816655644932 [线程一] log#3: 数据处理结束
简单总结一下,在 C++ 中:
- 如果需要定义函数级别的私有变量,使用普通局部变量
- 如果需要定义函数级别的共享变量,使用静态局部变量
- 如果需要定义程序级别的共享变量,可以使用全部变量
- 如果需要定义线程级别的私有变量,可以使用线程局部存储
2. 使用方法
我们先了解下 thread_local 如何定义线程局部变量。首先,它支持定义在全局作用域、类作用域、函数作用域的变量,如下所示:
#include <iostream>
// 全局
// 其他 cpp 文件可以访问
thread_local int a = 10;
// 只能当前 cpp 文件可以访问
static thread_local int b = 20;
// 类内
// 1. 必须定义为静态成员(static thread_local)
// 2. 只能类内声明,类外定义
class Demo {
public:
// 以下定义方式无效
// thread_local int c;
static thread_local int c; // 仅声明,定义需要在外部
};
// 必须在类外部定义:
thread_local int Demo::c = 30;
void test()
{
// 局部
// 下面两种写法等价
thread_local int d = 40;
static thread_local int e = 50;
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << Demo::c << std::endl;
std::cout << d << std::endl;
std::cout << e << std::endl;
}
int main()
{
test();
return 0;
}
同时,thread_local 也支持定义不同类型的变量,例如:基本类型、自定义类型、指针类型、引用类型。
#include <iostream>
#include <thread>
// 1. 基本类型
thread_local int number = 100;
void test01()
{
std::cout << number << std::endl;
}
// 2. 类对象
struct Demo { const int value = 100; };
thread_local Demo t_demo;
void test02()
{
std::cout << t_demo.value << std::endl;
}
// 3. 指针类型
thread_local Demo* p_demo = nullptr;
void test03()
{
if (nullptr == p_demo)
{
p_demo = new Demo;
}
std::cout << p_demo->value << std::endl;
if (p_demo != nullptr)
{
delete p_demo;
p_demo = nullptr;
}
}
// 4. 智能指针
thread_local std::unique_ptr<Demo> s_demo;
void test04()
{
if (!s_demo)
{
s_demo = std::make_unique<Demo>();
}
std::cout << s_demo->value << std::endl;
}
// 4. 引用类型(不建议)
Demo demo;
thread_local Demo& r_demo = demo;
void worker()
{
std::cout << std::this_thread::get_id() << " " << &r_demo << std::endl;
}
void test05()
{
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
}
int main()
{
test01();
return 0;
}
3. 生命周期
当定义普通全局、静态全局、类静态成员的 thread_local 变量时:
- MSVC 会为每个线程创建一份 thread_local 变量(即使该线程从未使用过这些变量),在线程结束时调用其析构函数
- GCC 会采用延迟初始化策略,只有当某个线程第一次访问
thread_local变量时,才会为其分配并初始化该变量。线程结束时会自动调用析构函数,但仅针对被初始化过的变量。
#include <iostream>
#include <thread>
#include <mutex>
struct Demo
{
Demo() { std::cout << std::this_thread::get_id() << " Demo 构造函数" << std::endl; }
~Demo() { std::cout << std::this_thread::get_id() << " Demo 析构函数" << std::endl; }
void print_demo() { std::cout << std::this_thread::get_id() << " print demo" << std::endl; }
};
#define FLAG 1
#if FLAG == 0
// 类静态成员 thread_local 变量
struct MyClass
{
static thread_local Demo demo;
};
thread_local Demo MyClass::demo;
#elif FLAG == 1
// 全局普通 thread_local 变量
thread_local Demo demo;
#else
// 全局静态 thread_local 变量
static thread_local Demo demo;
#endif
void worker()
{
std::cout << std::this_thread::get_id() << " 开始" << std::endl;
#if FLAG == 0
MyClass::demo.print_demo();
#else
demo.print_demo();
#endif
std::cout << std::this_thread::get_id() << " 结束" << std::endl;
}
void test()
{
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
std::cout << "worker 全部结束" << std::endl;
}
int main()
{
std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;
test();
return 0;
}
在函数内定义的 thread_local 变量,是每个线程私有的,仅在首次执行到该定义位置时构造;线程结束时,如果变量已被构造,则自动调用其析构函数。
注意:只有需要的时候才会被创建。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex lock;
struct Demo
{
Demo() { std::cout << std::this_thread::get_id() << " Demo 构造函数" << std::endl; }
~Demo() { std::cout << std::this_thread::get_id() << " Demo 析构函数" << std::endl; }
void print_demo() { std::cout << std::this_thread::get_id() << " print demo" << std::endl; }
};
void worker1()
{
std::lock_guard<std::mutex> guard(lock);
std::cout << "子线程ID:" << std::this_thread::get_id() << " worker 开始工作" << std::endl;
// 局部线程私有变量
thread_local Demo demo;
std::cout << "子线程ID:" << std::this_thread::get_id() << " worker 结束工作" << std::endl;
}
void worker2()
{
std::lock_guard<std::mutex> guard(lock);
std::cout << "子线程ID:" << std::this_thread::get_id() << " worker 开始工作" << std::endl;
std::cout << "子线程ID:" << std::this_thread::get_id() << " worker 结束工作" << std::endl;
}
void test()
{
std::thread t1(worker1);
std::thread t2(worker2);
t1.join();
t2.join();
std::cout << "worker 全部结束" << std::endl;
}
int main()
{
std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;
test();
return 0;
}
前面两种定义 thread_local 变量:
- 创建时机,会受到编译器的影响。对于全局和类内方式,MSVC 和 GCC 创建的时机不同。
- 销毁时机,线程结束时才会销毁
有时候我们想手动控制对象的创建和销毁时机,让不同的编译器行为一致,此时可以使用指针类型的线程局部变量。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex lock;
struct Demo
{
Demo() { std::cout << std::this_thread::get_id() << " Demo 构造函数" << std::endl; }
~Demo() { std::cout << std::this_thread::get_id() << " Demo 析构函数" << std::endl; }
void print_demo() { std::cout << std::this_thread::get_id() << " print demo" << std::endl; }
};
thread_local Demo* demo = nullptr;
void do_something()
{
// 在需要的时候创建
if (nullptr == demo)
{
demo = new Demo;
}
demo->print_demo();
}
void worker1()
{
do_something();
do_something();
// 不需要的时候析构
delete demo;
demo = nullptr;
std::cout << "再做一些其他事情" << std::endl;
}
void worker2()
{
std::cout << "随便做点事情" << std::endl;
}
void test()
{
std::thread t1(worker1);
std::thread t2(worker2);
t1.join();
t2.join();
}
int main()
{
test();
return 0;
}
4. 使用注意
在使用线程局部变量时,最需要注意的是在线程池中的使用。我们先简单回顾下线程池的概念,线程池的核心思想是预先创建固定数量的线程,并通过任务队列将需要执行的任务提交给这些线程处理。这样可以避免频繁地创建和销毁线程带来的性能开销,提高资源利用率和系统的响应能力。
线程池的一个重要特点是线程的复用:当一个任务执行完成后,线程不会被销毁,而是被保留用于执行下一个任务。虽然任务之间是相互独立的,但由于它们可能被同一个线程连续执行,因此在某些情况下,线程内部状态的残留可能对后续任务产生影响。
由于线程被复用,thread_local 中保存的变量也会随线程持续存在。如果一个任务在处理过程中设置了某个 thread_local 变量的值,而没有在任务结束时进行清理或重置,那么这个值将在该线程中保留下来。当下一个任务被分配到这个线程时,它访问的将是上一个任务遗留下来的状态,可能会导致逻辑错误或数据污染。
因此,在使用线程池处理任务时,如果涉及 thread_local 变量,应当在每个任务执行结束后主动清除或重置其内容,以避免线程状态泄漏对后续任务产生影响。


冀公网安备13050302001966号