创建线程
cpp
// createThread.cpp
#include <iostream>
#include <thread>
void helloFunction() {
std::cout << "Hello from a function." << std::endl;
}
class HelloFUncitonObject {
public:
void operator()()const {
std::cout << "Hello from a function object." << std::endl;
}
};
int main() {
std::cout << std::endl;
std::thread t1(helloFunction);
HelloFUncitonObject helloFunctionObject;
std::thread t2(helloFunctionObject);
std::thread t3([] {std::cout << "Hello from a lambda." << std::endl; });
t1.join();
t2.join();
t3.join();
std::cout << std::endl;
}
cpp
Hello from a function.
Hello from a function object.
Hello from a lambda.
C++11 线程生命周期
C++11 通过 <thread> 标准库提供原生线程支持,其线程生命周期没有 Java 那样严格的枚举状态(如 NEW、RUNNABLE 等) ,而是以「std::thread 对象」与「底层操作系统线程」的关联关系为核心,分为多个关键阶段。下面结合 C++11 特性、代码示例和核心关键点展开说明。
1、C++11 线程核心生命周期阶段
阶段1:std::thread 对象创建(无关联底层线程)
- 含义 :通过
std::thread的默认构造函数创建对象,此时对象不关联任何底层操作系统线程,处于「非可连接状态」。 - 触发条件 :调用
std::thread()默认构造函数。 - 关键点:该阶段仅创建线程对象(C++ 层面的封装),未分配任何系统线程资源,也不会执行任何业务逻辑。
- 代码示例:
cpp
#include <iostream>
#include <thread> // C++11 线程库头文件
using namespace std;
int main() {
// 阶段1:创建std::thread对象,无关联底层线程
thread empty_thread; // 默认构造函数,空线程对象
// 验证:判断线程是否可连接(此时返回false,无关联底层线程)
if (empty_thread.joinable()) {
cout << "empty_thread 关联了底层线程" << endl;
} else {
cout << "empty_thread 未关联底层线程(初始状态)" << endl;
}
return 0;
}
阶段2:线程启动(关联并启动底层操作系统线程)
- 含义 :当
std::thread对象通过「带可调用对象的构造函数」初始化时,会立即自动启动底层操作系统线程 ,线程进入运行就绪状态,等待 CPU 调度执行可调用对象逻辑(与 Java 手动调用start()启动线程的机制完全不同)。 - 触发条件 :向
std::thread构造函数传入有效可调用对象(函数、lambda 表达式、函数对象、绑定函数等)。 - 核心关键点 :
- C++11 线程无需手动调用「启动方法」,构造对象+传入可调用对象 = 立即启动线程,这是与 Java 线程的核心差异。
- 可调用对象的参数需通过
std::thread构造函数后续参数传递。
- 代码示例:
cpp
#include <iostream>
#include <thread>
#include <chrono> // 时间相关头文件
using namespace std;
// 自定义可调用函数(线程执行逻辑)
void thread_task(int num) {
cout << "底层线程启动,执行任务:" << num << endl;
// 模拟线程执行耗时操作
this_thread::sleep_for(chrono::milliseconds(100));
}
int main() {
// 阶段2:构造std::thread对象时传入可调用对象,立即启动底层线程
thread work_thread(thread_task, 100); // 传入函数+参数,线程自动启动
// 验证:此时线程对象可连接(关联了底层运行线程)
if (work_thread.joinable()) {
cout << "work_thread 已关联并启动底层线程" << endl;
}
// 等待线程执行完毕(后续阶段详细说明)
work_thread.join();
return 0;
}
阶段3:线程运行/阻塞(核心执行阶段)
该阶段包含两种子状态,线程会在两者之间切换:
3.1 运行状态
- 含义:线程获取 CPU 执行权,正在执行可调用对象的业务逻辑。
- 关键点:CPU 采用抢占式调度,线程执行时间由操作系统决定,C++11 无法主动控制线程的执行优先级(可通过系统 API 扩展,非标准库特性)。
3.2 阻塞状态
- 含义:线程暂时放弃 CPU 执行权,暂停执行,等待阻塞条件解除后重新进入就绪状态,等待 CPU 调度。
- C++11 中常见阻塞场景(附示例) :
- 时间阻塞:
std::this_thread::sleep_for/sleep_until(按时间阻塞) - 锁竞争阻塞:
std::mutex竞争失败(等待锁释放) - 条件阻塞:
std::condition_variable等待条件满足
- 时间阻塞:
- 关键点:阻塞时线程不占用 CPU 资源,仅消耗少量内存资源(用于保存线程上下文)。
- 代码示例(三种阻塞场景):
cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mtx; // 全局互斥锁
condition_variable cv; // 全局条件变量
bool is_ready = false; // 条件标志
// 场景1:时间阻塞
void sleep_block_task() {
cout << "进入时间阻塞前" << endl;
// 阻塞1秒(放弃CPU,1秒后自动唤醒)
this_thread::sleep_for(chrono::seconds(1));
cout << "时间阻塞结束,恢复执行" << endl;
}
// 场景2:锁竞争阻塞
void mutex_block_task(int id) {
// 尝试获取锁,若锁已被占用,则阻塞等待
lock_guard<mutex> lock(mtx); // RAII 方式管理锁,自动释放
cout << "线程" << id << "获取锁,执行临界区逻辑" << endl;
this_thread::sleep_for(chrono::milliseconds(500)); // 模拟临界区耗时
cout << "线程" << id << "释放锁,退出临界区" << endl;
}
// 场景3:条件变量阻塞
void cond_block_task() {
unique_lock<mutex> lock(mtx);
cout << "进入条件阻塞,等待条件满足" << endl;
// 阻塞等待,直到is_ready为true(会自动释放锁,被唤醒时重新获取锁)
cv.wait(lock, [](){ return is_ready; });
cout << "条件满足,条件阻塞结束" << endl;
}
int main() {
// 1. 时间阻塞示例
thread t1(sleep_block_task);
t1.join();
// 2. 锁竞争阻塞示例(两个线程竞争同一把锁)
thread t2(mutex_block_task, 1);
thread t3(mutex_block_task, 2);
t2.join();
t3.join();
// 3. 条件变量阻塞示例
thread t4(cond_block_task);
// 主线程延迟1秒后,设置条件并通知阻塞线程
this_thread::sleep_for(chrono::seconds(1));
{
lock_guard<mutex> lock(mtx);
is_ready = true;
}
cv.notify_one(); // 通知单个阻塞线程
t4.join();
return 0;
}
阶段4:线程终止(底层线程执行结束)
- 含义 :线程的可调用对象执行完毕,或因异常导致执行中断,底层操作系统线程终止运行,释放部分系统资源(但
std::thread对象仍处于「可连接状态」)。 - 两种终止场景 :
- 正常终止:可调用对象逻辑执行完毕(最常见场景)。
- 异常终止:线程内抛出未捕获的异常(C++11 中会导致程序调用
std::terminate()崩溃,需在线程内捕获所有异常)。
- 核心关键点 :线程终止后,必须通过
join()或detach()解除std::thread对象与底层线程的关联,否则线程对象析构时会崩溃。 - 代码示例(正常终止 + 异常终止处理):
cpp
#include <iostream>
#include <thread>
#include <exception>
using namespace std;
// 正常终止的线程任务
void normal_terminate_task() {
cout << "线程开始执行,即将正常终止" << endl;
}
// 可能异常终止的线程任务
void exception_terminate_task() {
try {
// 主动抛出异常
throw runtime_error("线程内部发生异常");
} catch (const exception& e) {
// 线程内捕获异常,避免程序崩溃
cout << "线程捕获异常:" << e.what() << ",异常处理后正常终止" << endl;
}
}
int main() {
// 正常终止示例
thread t1(normal_terminate_task);
t1.join(); // 等待线程正常终止
// 异常终止(已捕获)示例
thread t2(exception_terminate_task);
t2.join();
return 0;
}
阶段5:线程对象析构(释放C++层面资源)
- 含义 :
std::thread对象生命周期结束(超出作用域或手动析构),释放其自身占用的 C++ 内存资源。 - 核心关键点(C++11 线程必知坑点) :
- 若
std::thread对象处于「可连接状态」(关联了底层线程且未调用join()/detach()),析构时会调用std::terminate()导致程序崩溃。 - 两种安全处理方式:
join():阻塞主线程,等待底层线程终止后,解除对象与线程的关联(对象变为「非可连接状态」)。detach():将对象与底层线程分离,底层线程变为「后台线程」(由操作系统管理生命周期),对象析构不影响底层线程,线程执行完毕后由操作系统自动释放资源。
- 若
- 代码示例(崩溃场景 + 安全处理场景):
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void long_time_task() {
this_thread::sleep_for(chrono::seconds(2)); // 模拟长时间任务
}
int main() {
// 【危险场景】:线程对象析构时仍处于可连接状态,导致程序崩溃
// {
// thread bad_thread(long_time_task);
// // 未调用join()/detach(),bad_thread超出作用域析构时崩溃
// }
// 【安全场景1】:使用join()等待线程终止
{
thread join_thread(long_time_task);
cout << "主线程等待join_thread执行完毕..." << endl;
join_thread.join(); // 阻塞等待,线程终止后对象析构
cout << "join_thread已终止,对象安全析构" << endl;
}
// 【安全场景2】:使用detach()分离线程
{
thread detach_thread(long_time_task);
cout << "将detach_thread与底层线程分离..." << endl;
detach_thread.detach(); // 分离后,对象析构不影响底层线程
cout << "detach_thread对象安全析构,底层线程后台执行" << endl;
}
// 主线程延迟3秒,确保detach的线程执行完毕
this_thread::sleep_for(chrono::seconds(3));
return 0;
}
2、C++11 线程生命周期完整示例
下面的示例整合了所有生命周期阶段,展示从线程对象创建、启动、运行、阻塞到终止、析构的完整流程:
cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;
mutex mtx;
void full_lifecycle_task(int thread_id) {
// 阶段3:线程运行 + 阻塞
{
lock_guard<mutex> lock(mtx);
cout << "线程" << thread_id << ":进入运行状态" << endl;
}
// 时间阻塞
this_thread::sleep_for(chrono::milliseconds(800));
{
lock_guard<mutex> lock(mtx);
cout << "线程" << thread_id << ":阻塞结束,继续运行" << endl;
}
// 阶段4:可调用对象执行完毕,线程正常终止
}
int main() {
// 阶段1:创建空的thread对象(无关联底层线程)
thread empty_thread;
cout << "初始空线程对象是否可连接:" << boolalpha << empty_thread.joinable() << endl;
// 阶段2:构造对象并传入可调用对象,立即启动底层线程
thread work_thread(full_lifecycle_task, 1);
cout << "工作线程是否可连接:" << boolalpha << work_thread.joinable() << endl;
// 主线程执行逻辑
this_thread::sleep_for(chrono::milliseconds(400));
{
lock_guard<mutex> lock(mtx);
cout << "主线程:运行中,等待工作线程终止" << endl;
}
// 阶段5:调用join(),等待线程终止后安全析构
work_thread.join();
cout << "工作线程已终止,线程对象即将析构" << endl;
return 0;
}
3、C++11 线程生命周期核心关键点总结
- 启动差异 :C++11 线程无需手动调用
start(),std::thread构造时传入可调用对象即自动启动底层线程(与 Java 线程核心区别)。 - 可连接状态 :
std::thread对象的「可连接性」是核心,joinable()返回true时,必须调用join()或detach(),否则对象析构崩溃。 - 阻塞场景 :C++11 标准库支持三种常见阻塞:时间阻塞(
sleep_for)、锁竞争阻塞(std::mutex)、条件阻塞(std::condition_variable)。 - 终止处理 :线程内未捕获的异常会导致程序崩溃,需在线程内部做好异常捕获;线程正常终止后,通过
join()回收资源。 - 分离线程 :
detach()后的线程变为后台线程,生命周期由操作系统管理,C++ 程序退出时会强制终止所有后台线程。
thread 参数模板
在 C++ 中,std::thread 作为可变参数模板,允许通过复制、移动或引用的方式向线程函数传递参数。但当线程通过引用获取参数时,若不注意参数的生命周期和数据共享方式,极易引发未定义行为(如访问已销毁的内存、数据竞争等)。以下是具体分析和注意事项:
1. 线程通过引用获取参数的"陷阱":生命周期不匹配
std::thread 在构造时,会将传递的参数复制或移动到线程的内部存储 中,然后线程函数会接收这些内部副本的引用(若参数声明为引用)。这意味着:
如果原参数(如局部变量)的生命周期短于线程的运行周期,当原参数被销毁后,线程函数中的引用会变成悬垂引用,访问该引用会导致未定义行为。
错误示例:局部变量的引用被线程访问
cpp
#include <thread>
#include <iostream>
void thread_func(int& num) {
// 若主线程的 num 已销毁,此处访问会触发未定义行为
std::cout << "Thread: " << num << std::endl;
}
int main() {
{
int num = 10; // 局部变量,仅在当前作用域有效
std::thread t(thread_func, std::ref(num)); // 用 std::ref 传递引用
t.detach(); // 线程与主线程分离,可能在 num 销毁后运行
} // num 在此处销毁
// 主线程继续运行,可能此时子线程才开始执行 thread_func
return 0;
}
问题 :num 是局部变量,在离开作用域后被销毁,但子线程可能尚未执行(detach 后线程独立运行),此时 thread_func 中对 num 的引用已无效。
2. 正确处理引用参数的生命周期
若需通过引用向线程传递参数,必须确保原参数的生命周期长于线程的运行周期。常见解决方案:
方案1:确保参数生命周期覆盖线程运行期
cpp
#include <thread>
#include <iostream>
void thread_func(int& num) {
std::cout << "Thread: " << num << std::endl; // 安全:num 仍有效
}
int main() {
int num = 10; // 生命周期覆盖整个主线程
std::thread t(thread_func, std::ref(num));
t.join(); // 等待子线程执行完毕(确保 num 未销毁)
return 0;
}
关键 :用 join() 等待线程结束,保证原参数 num 在子线程执行期间始终有效。
方案2:使用动态分配的对象(配合智能指针)
若参数需长期共享,可使用 std::shared_ptr 管理动态对象,确保对象在所有引用它的线程结束前不被销毁:
cpp
#include <thread>
#include <iostream>
#include <memory>
void thread_func(std::shared_ptr<int> num_ptr) {
std::cout << "Thread: " << *num_ptr << std::endl; // 安全:对象由 shared_ptr 管理
}
int main() {
auto num_ptr = std::make_shared<int>(10); // 动态对象,生命周期由 shared_ptr 控制
std::thread t(thread_func, num_ptr); // 复制 shared_ptr(引用计数+1)
t.detach(); // 即使主线程结束,shared_ptr 仍保证对象存活
return 0;
}
3. 数据共享的同步问题
即使参数生命周期没问题,多个线程通过引用共享数据时,还需注意数据竞争 (多个线程同时读写共享数据)。需通过互斥锁(std::mutex)等同步机制保护:
cpp
#include <thread>
#include <iostream>
#include <mutex>
std::mutex mtx; // 互斥锁保护共享数据
int shared_num = 0;
void thread_func() {
for (int i = 0; i < 10000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁/解锁
shared_num++; // 安全修改共享数据
}
}
int main() {
std::thread t1(thread_func);
std::thread t2(thread_func);
t1.join();
t2.join();
std::cout << "Final: " << shared_num << std::endl; // 预期 20000
return 0;
}
总结
线程通过引用获取参数时,核心注意事项:
- 生命周期 :确保被引用的参数生命周期长于线程的运行周期(通过
join()等待或智能指针管理)。 - 显式传递引用 :需用
std::ref显式标记引用传递(否则std::thread会默认复制参数)。 - 数据同步:多线程共享数据时,通过互斥锁等机制避免数据竞争。
thread 的成员函数
std::thread 是 C++11 提供的原生线程封装类,其成员函数用于管理线程的生命周期、获取线程信息及线程间的基础操作。
1、构造函数(线程对象创建)
std::thread 提供多种构造函数,用于创建线程对象,核心特性是不可拷贝,仅可移动。
1. 默认构造函数
- 功能:创建一个空的线程对象,不关联任何底层操作系统线程,处于「非可连接状态」。
- 函数原型 :
thread() noexcept; - 示例:
cpp
#include <iostream>
#include <thread>
using namespace std;
int main() {
// 默认构造:空线程对象,无关联底层线程
thread empty_thread;
// 验证:非可连接状态
cout << "空线程对象是否可连接:" << boolalpha << empty_thread.joinable() << endl; // 输出 false
return 0;
}
2. 带可调用对象的构造函数(核心)
- 功能 :创建线程对象的同时,立即自动启动底层操作系统线程,执行传入的可调用对象(函数、lambda、函数对象等),线程进入就绪状态。
- 函数原型 :
template <class Fn, class... Args> explicit thread(Fn&& fn, Args&&... args); - 示例:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
// 自定义线程任务函数
void thread_task(int num, const string& msg) {
cout << "线程任务执行:num=" << num << ", msg=" << msg << endl;
this_thread::sleep_for(chrono::milliseconds(200)); // 模拟耗时操作
}
int main() {
// 构造线程对象时传入可调用对象+参数,立即启动线程
thread work_thread(thread_task, 100, "Hello C++11 Thread");
cout << "工作线程是否可连接:" << boolalpha << work_thread.joinable() << endl; // 输出 true
work_thread.join(); // 等待线程执行完毕
return 0;
}
- 关键注意事项 :
- 无需手动调用「启动方法」(与 Java
Thread.start()不同),构造即启动。 - 可调用对象的参数通过构造函数后续参数传递,默认按「值传递」,需引用传递时必须用
std::ref/std::cref。
- 无需手动调用「启动方法」(与 Java
3. 移动构造函数
- 功能 :转移一个
std::thread对象的底层线程所有权,原线程对象变为空的非可连接状态。 - 函数原型 :
thread(thread&& other) noexcept; - 示例:
cpp
#include <iostream>
#include <thread>
using namespace std;
void simple_task() {
cout << "移动后的线程执行任务" << endl;
}
int main() {
thread t1(simple_task); // 创建并启动线程
cout << "t1 线程ID:" << t1.get_id() << endl;
// 移动构造:将t1的线程所有权转移给t2
thread t2(move(t1));
cout << "t2 线程ID:" << t2.get_id() << endl;
cout << "t1 是否可连接:" << boolalpha << t1.joinable() << endl; // 输出 false(t1已空)
t2.join();
return 0;
}
- 关键注意事项 :
std::thread无拷贝构造函数(被禁用),不能通过thread t2(t1)拷贝线程对象,仅能通过std::move转移所有权。
2、析构函数
- 功能 :销毁
std::thread对象,释放其占用的 C++ 层面资源。 - 函数原型 :
~thread(); - 核心注意事项(致命坑点) :
若线程对象销毁时处于「可连接状态」(joinable() == true,即关联了底层线程且未调用join()/detach()),析构函数会调用std::terminate()导致程序直接崩溃。 - 错误示例(崩溃):
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void long_task() {
this_thread::sleep_for(chrono::seconds(1));
}
int main() {
{
thread bad_thread(long_task); // 启动线程
// 未调用 join()/detach(),对象析构时崩溃
}
return 0;
}
- 安全示例:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void long_task() {
this_thread::sleep_for(chrono::seconds(1));
}
int main() {
{
thread good_thread(long_task);
good_thread.join(); // 等待线程结束,解除可连接状态
} // 对象安全析构
return 0;
}
3、核心成员函数(线程管理)
1. joinable()
- 功能 :判断当前
std::thread对象是否处于「可连接状态」(即是否关联了一个未终止/未分离的底层线程)。 - 函数原型 :
bool joinable() const noexcept; - 返回值 :
true:对象关联了有效底层线程(已启动、未终止、未分离)。false:空对象(默认构造)、已调用join()/detach()、已移动所有权的对象。
- 示例:
cpp
#include <iostream>
#include <thread>
using namespace std;
void task() {}
int main() {
thread t1; // 默认构造
cout << "t1.joinable():" << boolalpha << t1.joinable() << endl; // false
thread t2(task); // 启动线程
cout << "t2.joinable()(启动后):" << boolalpha << t2.joinable() << endl; // true
t2.join(); // 等待线程结束
cout << "t2.joinable()(join后):" << boolalpha << t2.joinable() << endl; // false
thread t3(task);
t3.detach(); // 分离线程
cout << "t3.joinable()(detach后):" << boolalpha << t3.joinable() << endl; // false
return 0;
}
- 关键注意事项 :
所有线程管理操作(join()/detach())前,建议先通过joinable()判断,避免非法调用。
2. join()
- 功能 :阻塞当前调用线程 (通常是主线程),等待目标线程(
std::thread对象关联的线程)执行完毕后,解除两者的关联关系,使目标线程对象变为非可连接状态。 - 函数原型 :
void join(); - 示例:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void child_task() {
cout << "子线程开始执行,耗时2秒" << endl;
this_thread::sleep_for(chrono::seconds(2));
cout << "子线程执行完毕" << endl;
}
int main() {
cout << "主线程启动子线程" << endl;
thread child_thread(child_task);
cout << "主线程阻塞,等待子线程结束..." << endl;
child_thread.join(); // 阻塞主线程,直到子线程执行完毕
cout << "主线程继续执行,程序退出" << endl;
return 0;
}
- 关键注意事项 :
- 仅能对「可连接状态」的线程调用一次
join(),多次调用会抛出std::system_error异常。 - 调用
join()后,线程对象的get_id()会返回无效ID,joinable()返回false。 join()会回收底层线程的资源,避免内存泄漏。
- 仅能对「可连接状态」的线程调用一次
3. detach()
- 功能 :将
std::thread对象与底层线程「分离」,底层线程变为后台线程(由操作系统独立管理生命周期),线程对象变为非可连接状态。 - 函数原型 :
void detach(); - 示例:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void background_task() {
cout << "后台线程启动,耗时3秒" << endl;
this_thread::sleep_for(chrono::seconds(3));
cout << "后台线程执行完毕(无需主线程等待)" << endl;
}
int main() {
cout << "主线程启动后台线程" << endl;
thread bg_thread(background_task);
cout << "主线程与后台线程分离" << endl;
bg_thread.detach(); // 分离线程,对象与底层线程解除关联
cout << "主线程继续执行,延迟4秒确保后台线程执行完毕" << endl;
this_thread::sleep_for(chrono::seconds(4)); // 主线程阻塞等待
cout << "主线程退出" << endl;
return 0;
}
- 关键注意事项 :
- 仅能对「可连接状态」的线程调用一次
detach(),多次调用会抛出std::system_error异常。 - 分离后,线程对象无法再获取底层线程的信息(如
get_id()无效),也无法通过join()等待其结束。 - 后台线程若访问已销毁的资源(如局部变量),会导致未定义行为(悬垂引用),需确保共享资源生命周期覆盖后台线程。
- 程序退出时,所有后台线程会被操作系统强制终止,可能导致资源未正确释放(如文件未关闭)。
- 仅能对「可连接状态」的线程调用一次
4. get_id()
- 功能 :获取
std::thread对象关联线程的唯一标识(std::thread::id类型),空对象/已分离/已join的线程返回无效ID。 - 函数原型 :
thread::id get_id() const noexcept; - 示例:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void task() {
// 获取当前线程ID(this_thread 命名空间)
cout << "子线程ID:" << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
int main() {
// 获取主线程ID
cout << "主线程ID:" << this_thread::get_id() << endl;
thread t1(task);
thread t2(task);
// 获取线程对象关联的线程ID
cout << "t1 关联线程ID:" << t1.get_id() << endl;
cout << "t2 关联线程ID:" << t2.get_id() << endl;
t1.join();
t2.join();
// join后,线程对象ID无效
cout << "t1 join后的ID:" << t1.get_id() << endl; // 输出 0
return 0;
}
- 关键注意事项 :
std::thread::id可用于打印(直接输出)、比较(判断两个线程是否为同一个),但无法作为整数直接运算。
5. swap()
- 功能 :交换两个
std::thread对象的底层线程所有权,即互换它们的关联关系。 - 函数原型 :
void swap(thread& other) noexcept; - 示例:
cpp
#include <iostream>
#include <thread>
using namespace std;
void task1() {
cout << "线程1执行任务" << endl;
}
void task2() {
cout << "线程2执行任务" << endl;
}
int main() {
thread t1(task1);
thread t2(task2);
cout << "交换前:" << endl;
cout << "t1 ID:" << t1.get_id() << endl;
cout << "t2 ID:" << t2.get_id() << endl;
t1.swap(t2); // 交换t1和t2的线程所有权
cout << "交换后:" << endl;
cout << "t1 ID:" << t1.get_id() << endl; // 原t2的ID
cout << "t2 ID:" << t2.get_id() << endl; // 原t1的ID
t1.join();
t2.join();
return 0;
}
- 关键注意事项 :
交换后,两个线程对象的「可连接状态」也会随之互换,无需担心线程资源泄露。
4、静态成员函数
1. hardware_concurrency()
- 功能:获取当前硬件支持的「并发线程数」(通常等于CPU逻辑核心数),用于指导线程池创建等场景。
- 函数原型 :
static unsigned int hardware_concurrency() noexcept; - 示例:
cpp
#include <iostream>
#include <thread>
using namespace std;
int main() {
unsigned int core_num = thread::hardware_concurrency();
cout << "当前硬件逻辑核心数:" << core_num << endl;
cout << "建议创建的线程池大小:" << core_num << " 或 " << core_num + 1 << endl;
return 0;
}
- 关键注意事项 :
返回值是「提示性数值」,若硬件信息无法获取,会返回 0。
5、常用辅助函数(std::this_thread 命名空间)
虽非 std::thread 实例成员,但属于线程操作核心函数,高频使用:
std::this_thread::get_id():获取当前执行线程的ID(示例见get_id()部分)。std::this_thread::sleep_for():按指定时长阻塞当前线程(示例见前文)。std::this_thread::sleep_until():阻塞当前线程直到指定时间点。std::this_thread::yield():放弃当前CPU执行权,让操作系统调度其他就绪线程执行,示例:
cpp
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void yield_task(int id) {
for (int i = 0; i < 3; ++i) {
cout << "线程" << id << ":执行第" << i + 1 << "次任务" << endl;
this_thread::yield(); // 放弃CPU,让其他线程优先执行
}
}
int main() {
vector<thread> threads;
for (int i = 0; i < 2; ++i) {
threads.emplace_back(yield_task, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
6、综合示例(整合所有核心成员函数)
cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
using namespace std;
void thread_task(int id) {
cout << "线程" << id << "启动,ID:" << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500 * id)); // 按ID设置不同阻塞时间
cout << "线程" << id << "执行完毕" << endl;
}
int main() {
// 1. 静态成员函数:获取硬件核心数
cout << "硬件逻辑核心数:" << thread::hardware_concurrency() << endl;
// 2. 创建线程对象
vector<thread> thread_pool;
for (int i = 1; i <= 3; ++i) {
thread_pool.emplace_back(thread_task, i); // 带参构造,立即启动线程
}
// 3. 遍历线程池,打印线程信息
cout << "\n线程池中的线程信息:" << endl;
for (int i = 0; i < thread_pool.size(); ++i) {
cout << "线程" << i + 1 << ":可连接=" << thread_pool[i].joinable()
<< ",ID=" << thread_pool[i].get_id() << endl;
}
// 4. 交换两个线程的所有权
if (thread_pool.size() >= 2) {
cout << "\n交换线程1和线程2的所有权" << endl;
thread_pool[0].swap(thread_pool[1]);
cout << "交换后线程1 ID:" << thread_pool[0].get_id() << endl;
cout << "交换后线程2 ID:" << thread_pool[1].get_id() << endl;
}
// 5. 等待所有线程执行完毕
cout << "\n主线程等待所有子线程结束..." << endl;
for (auto& t : thread_pool) {
if (t.joinable()) { // 先判断,再join
t.join();
}
}
cout << "\n所有线程执行完毕,程序退出" << endl;
return 0;
}
7、关键注意事项总结
- 不可拷贝性 :
std::thread禁用拷贝构造函数,仅可通过std::move转移线程所有权。 - 可连接状态是核心 :
- 仅对
joinable() == true的线程调用join()/detach()。 - 线程对象销毁前,必须通过
join()/detach()解除可连接状态,否则程序崩溃。
- 仅对
- join() vs detach() :
- 需等待线程结果、回收资源:用
join()(推荐优先使用)。 - 无需等待线程、线程为后台任务:用
detach()(需注意资源生命周期)。
- 需等待线程结果、回收资源:用
- 参数传递 :
- 引用传递需用
std::ref/std::cref,否则默认按值拷贝。 - 避免向线程传递局部变量的引用(若线程生命周期长于局部变量,会导致悬垂引用)。
- 引用传递需用
- 异常安全 :线程内未捕获的异常会导致程序崩溃,需在线程内部用
try-catch捕获所有异常。 - 成员函数调用限制 :
join()/detach()/swap()仅能在当前线程对象未被移动、未被join/detach的状态下调用。
共享数据研究
1、 共享数据同步问题的核心成因
多线程环境下,共享数据的访问之所以会出现不一致问题,本质是由以下3个底层因素共同导致的:
1. 并发执行的时序不确定性
现代操作系统采用抢占式线程调度策略,CPU会在多个线程间以时间片轮转的方式快速切换。线程的执行顺序、执行时长完全不可控,这种不确定性导致多个线程对共享数据的访问顺序无法保证,进而引发数据冲突。
2. 共享数据操作的非原子性
原子操作 是指"不可被中断的最小操作单元",操作要么完全执行完毕,要么完全未执行,不存在中间状态。而C++中很多看似"单一"的操作,本质是由多条CPU指令组成的非原子操作,执行过程中可能被其他线程打断。
典型示例:i++ 拆解为3步核心操作(存在中间状态)
- 读操作:从内存中读取
i的值到CPU寄存器 - 改操作:在寄存器中对
i的值执行加1运算 - 写操作:将寄存器中的新值写回内存中的
i
这三步之间,CPU可能切换到其他线程,导致其他线程读取到i的中间值,最终引发数据错误。
3. 缓存一致性与指令重排问题
为提升CPU执行效率,现代计算机引入了多层缓存(L1/L2/L3)和指令重排优化,这在多线程环境下会引发两个问题:
- 缓存可见性问题:线程读取共享数据时,可能读取到CPU缓存中的旧副本,而非内存中的最新值(其他线程已更新但未同步到内存)
- 指令有序性问题:编译器和CPU会在保证单线程语义不变的前提下,对指令进行重排优化,但这种重排在多线程环境下会导致线程间看到的指令执行顺序不一致
注意:
volatile关键字无法解决多线程同步问题,它仅能防止编译器优化(保证变量每次从内存读取),但无法禁止CPU指令重排,也不能保证多线程间的缓存一致性。
2、 共享数据同步问题示例
下面通过两个可复现的示例,直观展示同步问题的表现及危害。
示例1: 非原子操作导致的计数器数据丢失
这是最经典的同步问题场景,多线程对全局计数器执行自增操作,最终结果远小于预期值。
cpp
#include <iostream>
#include <thread>
#include <vector>
// 共享全局计数器(非原子类型)
int g_counter = 0;
// 每个线程的自增次数
const int kIncrementTimes = 100000;
// 线程数量
const int kThreadNum = 10;
// 线程函数:执行kIncrementTimes次自增
void incrementCounter() {
for (int i = 0; i < kIncrementTimes; ++i) {
g_counter++; // 非原子操作,存在同步问题
}
}
int main() {
std::vector<std::thread> threads;
// 创建kThreadNum个线程
for (int i = 0; i < kThreadNum; ++i) {
threads.emplace_back(incrementCounter);
}
// 等待所有线程执行完毕
for (auto& t : threads) {
t.join();
}
// 预期结果:10 * 100000 = 1000000
std::cout << "预期计数器值:" << kThreadNum * kIncrementTimes << std::endl;
std::cout << "实际计数器值:" << g_counter << std::endl;
return 0;
}
运行结果分析
- 预期值:1000000
- 实际值:通常远小于1000000(如896543、952711等,每次运行结果不同)
- 问题根源:
g_counter++的非原子性,多个线程的"读-改-写"操作相互打断。例如:线程A和线程B同时读取到g_counter=0,各自加1后写回,最终仅完成1次有效自增,而非2次。
示例2: 无同步保护导致的共享队列异常
多线程操作共享队列(生产者入队、消费者出队),无同步保护时会出现程序崩溃或数据不一致。
cpp
#include <iostream>
#include <thread>
#include <queue>
// 共享任务队列
std::queue<int> g_task_queue;
const int kTaskNum = 1000;
// 生产者线程:入队任务
void producer() {
for (int i = 0; i < kTaskNum; ++i) {
g_task_queue.push(i); // 无同步保护的入队
}
}
// 消费者线程:出队任务
void consumer() {
for (int i = 0; i < kTaskNum; ++i) {
// 检查非空 和 出队 是两个独立操作,非原子性
if (!g_task_queue.empty()) {
g_task_queue.pop(); // 可能在队列已空时执行,导致程序崩溃
}
}
}
int main() {
std::thread t_producer(producer);
std::thread t_consumer(consumer);
t_producer.join();
t_consumer.join();
std::cout << "队列剩余元素数量:" << g_task_queue.size() << std::endl;
return 0;
}
运行结果分析
- 大概率出现程序崩溃(
std::queue在空队列上调用pop()是未定义行为) - 即使不崩溃,队列剩余元素数量也会异常(非预期值)
- 问题根源:"检查队列非空"和"执行出队"是两个独立操作,中间可能被其他线程打断。例如:消费者线程检查到队列非空,此时CPU切换到生产者线程,生产者线程执行了出队操作,当切回消费者线程时,队列已空,但仍会执行
pop()。
3、 解决方法
针对上述成因,C++提供了多种同步机制,从简单原子操作到复杂线程通信,覆盖不同场景需求。
方法1: 保证操作原子性------使用std::atomic(C++11及以上)
std::atomic是C++标准库提供的原子类型模板,支持对bool、整数类型、指针类型等进行原子操作,其核心特性:
- 操作不可中断:原子操作的"读-改-写"全程不会被其他线程打断
- 缓存可见性:操作结果立即同步到内存,其他线程可立即读取到最新值
- 禁止指令重排:支持指定内存序,避免有序性问题
- 无锁实现:大部分平台下,简单类型的
std::atomic是无锁的,效率高于互斥锁
修复示例1的代码(原子计数器)
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
// 原子类型计数器(初始化为0)
std::atomic<int> g_counter(0);
const int kIncrementTimes = 100000;
const int kThreadNum = 10;
void incrementCounter() {
for (int i = 0; i < kIncrementTimes; ++i) {
g_counter.fetch_add(1); // 原子自增,等价于g_counter++(针对atomic<int>)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < kThreadNum; ++i) {
threads.emplace_back(incrementCounter);
}
for (auto& t : threads) {
t.join();
}
std::cout << "预期计数器值:" << kThreadNum * kIncrementTimes << std::endl;
std::cout << "实际计数器值:" << g_counter.load() << std::endl; // 原子读取
return 0;
}
关键API说明
fetch_add(n):原子自增n,返回操作前的值fetch_sub(n):原子自减n,返回操作前的值load():原子读取当前值store(v):原子写入值v- 内存序指定:默认使用
memory_order_seq_cst(最强顺序一致性),可指定memory_order_relaxed(松散语义,效率更高,适用于无需有序性的场景)
方法2: 保护临界区------使用互斥锁(std::mutex + RAII封装)
临界区 是指多个线程共享的、需要独占访问的代码段(对共享数据的读写操作代码)。互斥锁(mutex)的核心作用是保证临界区的独占性:一个线程获取锁后,其他线程必须等待该线程释放锁才能进入临界区。
C++标准库提供的互斥锁及RAII封装(优先使用RAII,防止忘记解锁导致死锁):
std::mutex:基础互斥锁,支持lock()(阻塞获取锁)和unlock()(释放锁)std::lock_guard:简单RAII封装,构造时自动加锁,析构时自动释放锁(简单场景首选)std::unique_lock:灵活RAII封装,支持手动加锁/解锁、超时获取锁、与条件变量配合(复杂场景使用)
修复示例2的代码(互斥锁保护共享队列)
cpp
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
std::queue<int> g_task_queue;
std::mutex g_queue_mutex; // 保护队列的互斥锁
const int kTaskNum = 1000;
void producer() {
for (int i = 0; i < kTaskNum; ++i) {
// 构造时自动加锁,析构时自动解锁
std::lock_guard<std::mutex> lock(g_queue_mutex);
g_task_queue.push(i); // 临界区:独占访问队列
}
}
void consumer() {
for (int i = 0; i < kTaskNum; ++i) {
std::lock_guard<std::mutex> lock(g_queue_mutex);
// 临界区:检查非空 + 出队,原子性执行
if (!g_task_queue.empty()) {
g_task_queue.pop();
}
}
}
int main() {
std::thread t_producer(producer);
std::thread t_consumer(consumer);
t_producer.join();
t_consumer.join();
std::cout << "队列剩余元素数量:" << g_task_queue.size() << std::endl;
return 0;
}
方法3: 高级同步机制------解决复杂场景问题
1. std::condition_variable(条件变量)
作用:解决"忙等"问题(如消费者循环检查队列是否非空,浪费CPU资源),实现线程间通信(生产者通知消费者有新任务)。
核心特性:
- 必须与
std::unique_lock配合使用 wait(lock, condition):阻塞线程,释放锁,直到条件满足被唤醒notify_one():唤醒一个等待的线程notify_all():唤醒所有等待的线程
2. std::shared_mutex(读写锁,C++17及以上)
作用:针对"多读少写"场景优化并发效率(std::mutex是排他锁,读写均独占;读写锁支持两种锁类型):
- 共享锁(读锁):多个读线程可同时获取,提升读并发效率
- 排他锁(写锁):写线程独占访问,与共享锁、其他排他锁互斥
- 读线程使用
std::shared_lock(RAII封装共享锁) - 写线程使用
std::lock_guard/std::unique_lock(封装排他锁)
4、 实践用例
下面提供3个贴近实际开发的同步实践用例,覆盖高频场景。
实践用例1: 多读少写场景------配置信息同步(std::shared_mutex)
场景:系统配置(数据库地址、日志级别)被多个业务线程读取,偶尔被管理员线程更新,使用读写锁提升并发效率。
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <shared_mutex> // C++17及以上
// 系统配置结构体
struct SystemConfig {
std::string db_host = "127.0.0.1";
int db_port = 3306;
std::string log_level = "INFO";
};
SystemConfig g_sys_config;
std::shared_mutex g_config_mutex; // 读写锁
// 读线程:获取配置
void readConfig(int thread_id) {
// 共享锁:多个读线程可并发获取
std::shared_lock<std::shared_mutex> read_lock(g_config_mutex);
std::cout << "读线程" << thread_id << ":"
<< "DB_HOST=" << g_sys_config.db_host << ", "
<< "DB_PORT=" << g_sys_config.db_port << ", "
<< "LOG_LEVEL=" << g_sys_config.log_level << std::endl;
}
// 写线程:更新配置
void writeConfig() {
// 排他锁:独占访问,禁止其他读写线程进入
std::lock_guard<std::shared_mutex> write_lock(g_config_mutex);
std::cout << "\n写线程:更新系统配置" << std::endl;
g_sys_config.db_host = "192.168.1.100";
g_sys_config.db_port = 3307;
g_sys_config.log_level = "DEBUG";
}
int main() {
const int kReadThreadNum = 8; // 8个读线程(高并发读)
std::vector<std::thread> threads;
// 创建8个读线程
for (int i = 0; i < kReadThreadNum; ++i) {
threads.emplace_back(readConfig, i);
}
// 创建1个写线程
threads.emplace_back(writeConfig);
// 再创建8个读线程
for (int i = kReadThreadNum; i < 2 * kReadThreadNum; ++i) {
threads.emplace_back(readConfig, i);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
实践用例2: 经典生产者-消费者模型(mutex + condition_variable)
场景:生产者生成任务入队,消费者取出任务执行,使用条件变量避免忙等,提升CPU利用率。
cpp
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <vector>
std::queue<int> g_task_queue;
std::mutex g_queue_mutex;
std::condition_variable g_cv;
bool g_stop_flag = false; // 停止标志
// 生产者线程
void producer(int producer_id, int task_count) {
for (int i = 0; i < task_count; ++i) {
int task_id = producer_id * 1000 + i; // 唯一任务ID
{
std::lock_guard<std::mutex> lock(g_queue_mutex);
g_task_queue.push(task_id);
std::cout << "生产者" << producer_id << ":生成任务" << task_id << std::endl;
}
g_cv.notify_one(); // 通知一个消费者
}
}
// 消费者线程
void consumer(int consumer_id) {
while (true) {
std::unique_lock<std::mutex> lock(g_queue_mutex);
// 等待条件:队列非空 或 停止标志为true(防止虚假唤醒)
g_cv.wait(lock, []() {
return !g_task_queue.empty() || g_stop_flag;
});
// 退出条件:停止标志为true 且 队列为空
if (g_stop_flag && g_task_queue.empty()) {
std::cout << "消费者" << consumer_id << ":无任务,退出" << std::endl;
break;
}
// 取出任务
int task_id = g_task_queue.front();
g_task_queue.pop();
lock.unlock(); // 提前释放锁,提升并发效率
// 模拟任务执行
std::cout << "消费者" << consumer_id << ":执行任务" << task_id << std::endl;
}
}
int main() {
const int kProducerNum = 2;
const int kConsumerNum = 3;
const int kTaskPerProducer = 5;
std::vector<std::thread> producers;
std::vector<std::thread> consumers;
// 创建生产者
for (int i = 0; i < kProducerNum; ++i) {
producers.emplace_back(producer, i, kTaskPerProducer);
}
// 创建消费者
for (int i = 0; i < kConsumerNum; ++i) {
consumers.emplace_back(consumer, i);
}
// 等待生产者完成
for (auto& p : producers) {
p.join();
}
// 设置停止标志并通知所有消费者
{
std::lock_guard<std::mutex> lock(g_queue_mutex);
g_stop_flag = true;
}
g_cv.notify_all();
// 等待消费者完成
for (auto& c : consumers) {
c.join();
}
std::cout << "所有任务执行完毕" << std::endl;
return 0;
}
实践用例3: 无锁高效计数器(std::atomic)
场景:统计接口调用次数,多个线程并发调用接口,使用原子计数器实现无锁高效统计。
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
// 原子计数器:统计接口调用次数
std::atomic<size_t> g_api_call_count(0);
// 模拟接口调用
void apiCall(int thread_id, int call_times) {
for (int i = 0; i < call_times; ++i) {
// 松散语义:仅需原子性,无需有序性,效率最高
g_api_call_count.fetch_add(1, std::memory_order_relaxed);
std::this_thread::yield(); // 模拟业务逻辑耗时
}
std::cout << "线程" << thread_id << ":完成" << call_times << "次接口调用" << std::endl;
}
int main() {
const int kThreadNum = 10;
const int kCallPerThread = 10000;
std::vector<std::thread> threads;
// 创建10个线程
for (int i = 0; i < kThreadNum; ++i) {
threads.emplace_back(apiCall, i, kCallPerThread);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
// 原子读取计数器
std::cout << "\n接口总调用次数:" << g_api_call_count.load(std::memory_order_relaxed) << std::endl;
std::cout << "预期调用次数:" << kThreadNum * kCallPerThread << std::endl;
return 0;
}
5、 同步编程注意事项
- 最小化临界区:仅保护必要的共享数据操作,避免在临界区内执行IO、复杂计算等耗时操作
- 避免死锁 :
- 按固定顺序给多个互斥锁加锁
- 使用
std::lock()一次性锁定多个互斥锁 - 优先使用RAII封装(
lock_guard/unique_lock)
- 选择合适的同步机制 :
- 简单原子操作(计数器、标志位):优先用
std::atomic - 复杂共享数据(队列、结构体):用
std::mutex+ RAII - 多读少写场景:用
std::shared_mutex - 线程间通信:用
std::condition_variable
- 简单原子操作(计数器、标志位):优先用
- 禁止使用
volatile同步:它无法解决指令重排和缓存一致性问题 - 减少全局共享数据:尽量通过类成员变量、函数参数传递数据,降低同步复杂度
总结
- 共享数据同步问题的核心成因:时序不确定性、操作非原子性、缓存与指令重排
- 基础解决方案:
std::atomic(原子操作)、std::mutex(临界区保护) - 高级解决方案:
std::condition_variable(线程通信)、std::shared_mutex(多读少写优化) - 实践原则:最小化临界区、避免死锁、按需选择同步机制、优先无锁实现(简单场景)
线程的本地数据研究
线程本地数据(Thread-Local Storage,简称TLS,也叫线程局部变量 )是多线程编程中用于实现线程隔离数据的核心特性,下面我们先拆解其核心含义,再通过示例直观验证。
1、线程本地数据(TLS)核心本质
1. 与普通静态数据的「相似性」与「核心区别」
- 「相似于静态数据」:TLS变量的生命周期持久化,不会像普通局部变量那样随函数调用栈销毁,而是伴随线程的整个生命周期(线程创建后初始化,线程终止时销毁),这一点和静态数据(进程生命周期)的"持久化"特性一致。
- 「核心区别(线程隔离性)」:普通静态变量是进程全局唯一副本 ,所有线程共享该变量,多线程操作时会存在竞争条件;而TLS变量是每个线程拥有独立的私有副本,线程对自身副本的读写操作完全不受其他线程干扰,无需额外加锁(针对该变量本身)。
2. 不同声明场景的TLS变量生命周期(对应你提到的三种场景)
C++中通过thread_local关键字标识线程局部变量,其生命周期和创建时机随声明位置不同而变化,具体如下:
| 声明位置 | 创建时机 | 销毁时机 | 核心特性 |
|---|---|---|---|
| 命名空间范围内 | 线程内第一次使用该变量之前 | 线程终止时 | 线程内全局可见,每个线程独立副本 |
| 静态类成员 | 线程内第一次访问该成员之前 | 线程终止时 | 类级静态成员,但线程隔离,不跨线程共享 |
| 函数内(局部声明) | 线程内第一次调用函数并使用该变量时 | 线程终止时 | 函数内可见,但生命周期随线程,线程内复用 |
关键补充:
- 函数内的TLS变量:不是函数调用结束销毁,而是线程终止时销毁;线程内多次调用该函数,只会初始化一次,后续复用已创建的副本并保留其值。
- 所有TLS变量的核心共性:仅属于所属线程,线程间副本完全隔离,修改互不影响。
2、完整代码示例(C++)
下面的示例涵盖了三种声明场景,创建2个子线程分别操作TLS变量,直观验证线程隔离性。
cpp
#include <iostream>
#include <thread>
#include <chrono>
// 场景1:命名空间范围内的线程局部变量
namespace MyNamespace {
thread_local int g_tls_ns_var = 0; // 每个线程独立副本,初始值0
}
// 场景2:静态类成员的线程局部变量
class MyClass {
public:
static thread_local int s_tls_class_var; // 静态类成员,线程局部
static void incrClassVar() {
s_tls_class_var++; // 操作当前线程的私有副本
}
};
// 类静态TLS变量的外部初始化
thread_local int MyClass::s_tls_class_var = 0;
// 场景3:函数内的线程局部变量
void funcWithTls() {
thread_local int local_tls_var = 0; // 线程内第一次调用时初始化,后续复用
local_tls_var++; // 每次调用函数,当前线程的副本值自增
// 打印函数内TLS变量值
std::cout << "线程ID: " << std::this_thread::get_id()
<< " | 函数内TLS变量: " << local_tls_var << std::endl;
}
// 线程执行函数:操作三种TLS变量
void threadTask(int thread_idx) {
using namespace MyNamespace;
// 操作命名空间TLS变量
for (int i = 0; i < 2; i++) {
g_tls_ns_var++;
std::cout << "线程" << thread_idx << "(ID: " << std::this_thread::get_id()
<< ") | 命名空间TLS变量: " << g_tls_ns_var << std::endl;
}
// 操作静态类成员TLS变量
MyClass::incrClassVar();
MyClass::incrClassVar();
std::cout << "线程" << thread_idx << "(ID: " << std::this_thread::get_id()
<< ") | 类静态TLS变量: " << MyClass::s_tls_class_var << std::endl;
// 两次调用函数,验证函数内TLS变量的复用性
funcWithTls();
funcWithTls();
// 线程休眠,便于观察输出顺序
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
int main() {
// 创建2个子线程
std::thread t1(threadTask, 1);
std::thread t2(threadTask, 2);
// 等待子线程执行完毕
t1.join();
t2.join();
// 主线程操作TLS变量,验证主线程也有独立副本
std::cout << "\n主线程(ID: " << std::this_thread::get_id() << ")操作:" << std::endl;
MyNamespace::g_tls_ns_var = 100;
MyClass::s_tls_class_var = 200;
funcWithTls();
return 0;
}
3、示例运行结果与解读
1. 典型运行结果(线程ID因系统而异)
plain
线程1(ID: 0x70000f55b000) | 命名空间TLS变量: 1
线程1(ID: 0x70000f55b000) | 命名空间TLS变量: 2
线程1(ID: 0x70000f55b000) | 类静态TLS变量: 2
线程1(ID: 0x70000f55b000) | 函数内TLS变量: 1
线程1(ID: 0x70000f55b000) | 函数内TLS变量: 2
线程2(ID: 0x70000f5e4000) | 命名空间TLS变量: 1
线程2(ID: 0x70000f5e4000) | 命名空间TLS变量: 2
线程2(ID: 0x70000f5e4000) | 类静态TLS变量: 2
线程2(ID: 0x70000f5e4000) | 函数内TLS变量: 1
线程2(ID: 0x70000f5e4000) | 函数内TLS变量: 2
主线程(ID: 0x1000e5e00)操作:
主线程(ID: 0x1000e5e00) | 函数内TLS变量: 1
2. 关键结果解读(呼应TLS核心特性)
(1)线程隔离性:每个线程的TLS副本独立
线程1和线程2操作的g_tls_ns_var(命名空间)、s_tls_class_var(类静态)、local_tls_var(函数内)都是从0开始自增,最终线程1的三个变量值和线程2完全一致,但彼此互不干扰(线程1的变量自增不会影响线程2的初始值)。
(2)函数内TLS变量的复用性(生命周期随线程)
线程1两次调用funcWithTls(),local_tls_var从1变为2,说明该变量不是函数调用结束后销毁,而是线程内持久化,第一次调用初始化(0→1),第二次调用复用副本(1→2),验证了"函数内TLS变量在第一次使用时创建,线程终止时销毁"。
(3)主线程拥有独立副本
主线程中给g_tls_ns_var赋值100、s_tls_class_var赋值200,完全不影响子线程的变量值;同时主线程第一次调用funcWithTls(),local_tls_var初始化为0→1,说明主线程作为独立线程,也拥有自己的TLS变量副本。
(4)三种声明场景的创建时机差异
- 命名空间/类静态TLS变量:线程启动后,第一次使用前初始化(线程1执行
g_tls_ns_var++前,变量已初始化为0)。 - 函数内TLS变量:线程第一次调用函数并使用变量时初始化(线程1第一次调用
funcWithTls()时,local_tls_var才初始化为0),比前两种场景更晚(懒创建)。
总结
- TLS变量的核心是线程私有副本,行为类似静态数据(生命周期持久化),但不跨线程共享,无需针对变量本身加锁。
- 声明位置决定创建时机:命名空间/静态类成员(第一次使用前)< 函数内(第一次调用函数并使用时),销毁时机均为线程终止时。
- 函数内TLS变量的关键特性:线程内多次调用函数时复用副本,保留变量状态,不会随函数调用栈销毁。
- 主线程与子线程拥有各自独立的TLS副本,操作互不干扰,这是TLS与普通静态变量的核心区别。
条件变量研究
1、核心理解
1. 基本定义
C++11 引入的 std::condition_variable(位于 <condition_variable> 头文件)是线程间同步的核心工具 ,用于实现「线程等待某个条件满足,或被其他线程唤醒继续执行」的场景,其核心价值是避免线程忙等(自旋等待),大幅提升CPU利用率。
2. 核心特性
- 不能单独使用,必须配合
**std::mutex**(互斥锁)和「共享条件」(如队列非空、数据就绪等); - **本质是「事件驱动」同步,**区别于互斥锁的「资源互斥保护」(互斥锁解决「同时访问」问题,条件变量解决「等待条件满足」问题);
- 支持线程挂起与唤醒,挂起时会释放持有的互斥锁,避免阻塞其他线程。
2、底层原理
1. 操作系统依赖
C++ std::condition_variable 是操作系统内核同步机制的封装:
- Linux 下:封装 POSIX 线程库的
pthread_cond_t(条件变量)、pthread_cond_wait()(等待)、pthread_cond_signal()(唤醒单个)、pthread_cond_broadcast()(唤醒所有); - Windows 下:封装
CONDITION_VARIABLE相关 API(SleepConditionVariableCS等)。
2. 核心工作机制
- 等待队列 :条件变量内部维护一个「等待线程队列」,调用
wait()的线程会被加入该队列并挂起(从运行态进入阻塞态,放弃CPU使用权); - 锁的自动释放与重获取 :线程调用
wait()时,会自动释放传入的std::unique_lock所持有的互斥锁,让其他线程可以修改「共享条件」;当线程被唤醒时,会先尝试重新获取互斥锁,获取成功后才会从wait()返回,继续执行; - 伪唤醒(Spurious Wakeup) :操作系统内核可能因调度优化、信号中断等原因,在无其他线程调用
notify的情况下,唤醒等待队列中的线程(伪唤醒是底层特性,无法避免); - 唤醒调度 :
notify_one()随机唤醒队列中一个线程(调度策略由操作系统决定),notify_all()唤醒队列中所有线程,被唤醒的线程会竞争互斥锁,只有获取锁的线程能继续执行。
3、核心成员函数说明
条件变量的成员函数分为「等待类」和「唤醒类」,下面结合生产者-消费者模型(核心应用场景)进行详细说明。
前置准备:基础环境
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std;
// 共享资源:任务队列
queue<int> task_queue;
// 互斥锁:保护共享资源(任务队列)
mutex mtx;
// 条件变量:实现线程同步
condition_variable cv;
// 结束标志
bool is_finished = false;
1. 等待类函数
(1)wait(unique_lock<mutex>& lock)(无谓词版本)
- 功能 :挂起当前线程,加入条件变量等待队列;自动释放
lock持有的互斥锁;被唤醒后,自动重新获取互斥锁,获取成功后返回。 - 缺陷 :无法处理伪唤醒,被唤醒后无法保证条件已满足,不推荐直接使用。
- 示例(不推荐,仅作演示):
cpp
// 消费者线程函数(无谓词wait,存在风险)
void consumer_without_predicate() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 先手动判断条件(无法避免伪唤醒)
if (task_queue.empty()) {
// 挂起线程,释放锁;被唤醒后重新获取锁,直接返回
cv.wait(lock);
// 伪唤醒风险:此时task_queue仍可能为空
}
// 处理任务
int task = task_queue.front();
task_queue.pop();
cout << "消费者(无谓词)处理任务:" << task << endl;
lock.unlock(); // 手动释放锁(非必须,unique_lock析构时会自动释放)
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者(无谓词)退出" << endl;
}
(2)wait(unique_lock<mutex>& lock, Predicate pred)(带谓词版本,推荐)
- 功能 :等价于
while(!pred()) { wait(lock); },自动处理伪唤醒;只有当谓词pred返回true(条件满足)时,才会从wait()返回。 - 优势:无需手动写循环判断,底层已封装循环逻辑,彻底解决伪唤醒问题。
- 参数 :
pred是可调用对象(lambda、函数指针、函数对象),返回bool类型,代表「等待的条件是否满足」。 - 示例(推荐用法):
cpp
// 消费者线程函数(带谓词wait,推荐)
void consumer_with_predicate() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 谓词:任务队列非空 且 未结束
cv.wait(lock, []() {
return !task_queue.empty() || is_finished;
});
// 退出条件判断
if (is_finished && task_queue.empty()) {
break;
}
// 处理任务(此时条件一定满足:队列非空)
int task = task_queue.front();
task_queue.pop();
cout << "消费者(带谓词)处理任务:" << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者(带谓词)退出" << endl;
}
(3)wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred)(超时等待+谓词)
- 功能 :等待「指定时间长度」,若超时或条件满足(
pred返回true),则返回;同样封装循环逻辑,避免伪唤醒。 - 返回值 :
bool,true表示条件满足,false表示超时。 - 示例:消费者等待500毫秒,超时则打印提示:
cpp
// 消费者线程函数(超时等待)
void consumer_with_timeout() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 等待500毫秒,谓词:队列非空 或 已结束
bool condition_met = cv.wait_for(lock, chrono::milliseconds(500), []() {
return !task_queue.empty() || is_finished;
});
// 超时判断
if (!condition_met) {
cout << "消费者(超时)等待500ms超时,无任务可处理" << endl;
continue;
}
// 退出条件
if (is_finished && task_queue.empty()) {
break;
}
// 处理任务
int task = task_queue.front();
task_queue.pop();
cout << "消费者(超时)处理任务:" << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者(超时)退出" << endl;
}
(4)wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred)
- 功能 :与
wait_for类似,区别是「等待到指定时间点」(绝对时间),而非「指定时间长度」(相对时间)。 - 示例:等待到当前时间+1秒:
cpp
// 消费者线程函数(指定时间点等待)
void consumer_with_timepoint() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 绝对时间点:当前时间+1秒
auto wakeup_time = chrono::system_clock::now() + chrono::seconds(1);
bool condition_met = cv.wait_until(lock, wakeup_time, []() {
return !task_queue.empty() || is_finished;
});
if (!condition_met) {
cout << "消费者(时间点)等待1秒超时,无任务可处理" << endl;
continue;
}
if (is_finished && task_queue.empty()) {
break;
}
int task = task_queue.front();
task_queue.pop();
cout << "消费者(时间点)处理任务:" << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者(时间点)退出" << endl;
}
2. 唤醒类函数
(1)notify_one()
- 功能 :唤醒条件变量等待队列中的一个线程(具体唤醒哪个由操作系统调度决定,无确定性);若等待队列为空,该函数无任何效果。
- 适用场景:单个生产者对应单个消费者,或多个生产者/消费者但只需唤醒一个线程处理任务。
- 示例:生产者生产单个任务后,唤醒一个消费者:
cpp
// 生产者线程函数(notify_one)
void producer_with_notify_one(int start_task, int task_count) {
for (int i = 0; i < task_count; ++i) {
int task = start_task + i;
// 保护共享资源:修改任务队列
{
lock_guard<mutex> lock(mtx); // 此处可用lock_guard(仅加锁保护修改)
task_queue.push(task);
cout << "生产者(notify_one)生产任务:" << task << endl;
} // lock_guard析构,自动释放锁(推荐在临界区外唤醒,减少上下文切换)
// 唤醒一个消费者
cv.notify_one();
this_thread::sleep_for(chrono::milliseconds(200));
}
// 生产完毕,设置结束标志
{
lock_guard<mutex> lock(mtx);
is_finished = true;
}
cv.notify_one(); // 唤醒最后一个等待的消费者
cout << "生产者(notify_one)生产完毕" << endl;
}
(2)notify_all()
- 功能 :唤醒条件变量等待队列中的所有线程;所有被唤醒的线程会竞争互斥锁,只有一个线程能成功获取锁并执行,执行完毕释放锁后,其他线程继续竞争。
- 适用场景:多个消费者等待同一条件,需所有线程知晓「条件已满足」(如配置更新后,所有消费者需重新加载配置)。
- 示例:生产者生产一批任务后,唤醒所有消费者:
cpp
// 生产者线程函数(notify_all)
void producer_with_notify_all(int start_task, int task_count) {
// 批量生产任务
{
lock_guard<mutex> lock(mtx);
for (int i = 0; i < task_count; ++i) {
int task = start_task + i;
task_queue.push(task);
cout << "生产者(notify_all)生产任务:" << task << endl;
}
}
// 唤醒所有消费者
cv.notify_all();
this_thread::sleep_for(chrono::seconds(1));
// 生产完毕,设置结束标志
{
lock_guard<mutex> lock(mtx);
is_finished = true;
}
cv.notify_all(); // 唤醒所有等待的消费者
cout << "生产者(notify_all)生产完毕" << endl;
}
4、实际使用注意事项
1. 必须配合 std::unique_lock,禁止使用 std::lock_guard
- 原因:
wait()过程中需要手动解锁(挂起前)和重新加锁(唤醒后) ,std::lock_guard仅支持构造时加锁、析构时解锁,不支持手动控制锁状态;而std::unique_lock提供lock()/unlock()成员函数,满足条件变量的需求。 - 错误示例:
cv.wait(lock_guard<mutex>(mtx));(编译报错)。
2. 优先使用带谓词的 wait 重载,彻底解决伪唤醒
- 原因:伪唤醒无法避免,不带谓词的
wait被唤醒后直接返回,可能导致线程在条件不满足时继续执行,引发逻辑错误(如从空队列中取数据);带谓词的wait底层封装了while(!pred()) { wait(lock); },即使伪唤醒也会重新判断条件,不满足则继续等待。
3. 共享条件必须受互斥锁保护
- 原因:「共享条件」(如
task_queue.empty())是线程间共享的,若不通过互斥锁保护,会出现竞态条件(如消费者判断「队列空」后,还未调用wait(),生产者就插入数据并notify,导致消费者后续wait()永久阻塞)。 - 正确做法:所有对「共享条件」的读取和修改,都必须在互斥锁的临界区内进行。
4. 避免「丢失唤醒」(Lost Wakeup)
- 丢失唤醒场景:线程A先判断条件不满足,准备调用
wait(),但在调用wait()之前,线程B修改了条件并调用notify_one(),此时线程A还未进入等待队列,notify_one()无效果,线程A后续调用wait()会永久阻塞。 - 解决方法:在互斥锁保护下,原子性完成「条件判断」和「调用wait()」 (即条件判断和
wait()必须在同一个临界区内),确保线程A在判断条件后,其他线程无法修改条件和notify,直到线程A进入等待队列并释放锁。
5. notify 建议在临界区外调用
- 原因:若在临界区内(持有互斥锁时)调用
notify,被唤醒的线程会立即尝试获取互斥锁,但此时调用者还持有锁,导致被唤醒线程重新阻塞,增加上下文切换开销;在临界区外调用,调用者已释放锁,被唤醒线程可直接获取锁,效率更高。
6. 多条件场景使用多个条件变量
- 场景:生产者等待「队列非满」,消费者等待「队列非空」,若共用一个条件变量,会导致不必要的线程唤醒(如生产者唤醒消费者,消费者唤醒生产者,增加竞争开销)。
- 解决方法:为每个条件分配独立的
std::condition_variable(如cv_not_empty(消费者等待)、cv_not_full(生产者等待)),精准唤醒对应线程。
5、唤醒与未唤醒场景
场景1:正常唤醒(notify_one() + 带谓词 wait())
- 描述:1个生产者生产任务,1个消费者等待任务,生产者生产后调用
notify_one(),消费者被正常唤醒并处理任务。 - 完整代码:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std;
queue<int> task_queue;
mutex mtx;
condition_variable cv;
bool is_finished = false;
// 消费者(带谓词wait)
void consumer() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 带谓词wait,避免伪唤醒
cv.wait(lock, []() { return !task_queue.empty() || is_finished; });
if (is_finished && task_queue.empty()) break;
int task = task_queue.front();
task_queue.pop();
cout << "消费者:处理任务 " << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者:退出" << endl;
}
// 生产者(notify_one)
void producer() {
for (int i = 1; i <= 5; ++i) {
{
lock_guard<mutex> lock(mtx);
task_queue.push(i);
cout << "生产者:生产任务 " << i << endl;
}
// 唤醒一个消费者(正常唤醒)
cv.notify_one();
this_thread::sleep_for(chrono::milliseconds(200));
}
{
lock_guard<mutex> lock(mtx);
is_finished = true;
}
cv.notify_one();
cout << "生产者:生产完毕" << endl;
}
int main() {
thread t_consumer(consumer);
thread t_producer(producer);
t_consumer.join();
t_producer.join();
return 0;
}
- 运行效果:生产者每生产一个任务,消费者就被唤醒并处理,无遗漏、无错误。
场景2:正常唤醒(notify_all() + 多消费者)
- 描述:2个消费者等待任务,生产者批量生产3个任务后调用
notify_all(),两个消费者竞争互斥锁,依次处理任务。 - 关键代码(基于前置环境):
cpp
// 消费者1
void consumer1() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []() { return !task_queue.empty() || is_finished; });
if (is_finished && task_queue.empty()) break;
int task = task_queue.front();
task_queue.pop();
cout << "消费者1:处理任务 " << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者1:退出" << endl;
}
// 消费者2
void consumer2() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []() { return !task_queue.empty() || is_finished; });
if (is_finished && task_queue.empty()) break;
int task = task_queue.front();
task_queue.pop();
cout << "消费者2:处理任务 " << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "消费者2:退出" << endl;
}
int main() {
thread t1(consumer1);
thread t2(consumer2);
thread t_prod(producer_with_notify_all, 1, 3); // 生产任务1-3
t1.join();
t2.join();
t_prod.join();
return 0;
}
- 运行效果:两个消费者竞争处理3个任务,均被正常唤醒,无遗漏。
场景3:未唤醒 - 丢失唤醒(错误用法)
- 描述:消费者未在互斥锁保护下判断条件,导致生产者提前
notify,消费者后续wait()永久阻塞。 - 错误代码:
cpp
// 错误的消费者(条件判断未加锁)
void bad_consumer() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
// 错误:先解锁,再判断条件(出现竞态条件)
lock.unlock();
if (task_queue.empty()) {
lock.lock();
// 丢失唤醒风险:此处调用wait前,生产者可能已notify
cv.wait(lock);
}
lock.lock();
if (is_finished && task_queue.empty()) break;
int task = task_queue.front();
task_queue.pop();
cout << "错误消费者:处理任务 " << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
cout << "错误消费者:退出" << endl;
}
- 问题:消费者解锁后判断
task_queue.empty(),此时生产者可能插入数据并notify_one(),消费者后续加锁并wait(),但notify已提前执行,导致消费者永久阻塞(未被唤醒)。 - 修正:条件判断必须在互斥锁临界区内,原子性完成「判断+wait」。
场景4:未唤醒 - 唤醒无效(等待队列为空时 notify)
- 描述:生产者先调用
notify_one(),再生产任务,消费者后调用wait(),此时notify无效果,消费者需等待后续notify(若没有则永久阻塞)。 - 错误代码:
cpp
// 错误的生产者(先notify,后生产)
void bad_producer() {
cv.notify_one(); // 此时无消费者等待,notify无效
{
lock_guard<mutex> lock(mtx);
task_queue.push(1);
cout << "错误生产者:生产任务 1" << endl;
}
// 无后续notify,消费者永久阻塞
this_thread::sleep_for(chrono::seconds(1));
is_finished = true;
cout << "错误生产者:生产完毕" << endl;
}
- 问题:生产者
notify时,消费者尚未调用wait()(等待队列为空),notify无任何效果,消费者后续wait()无法被唤醒,永久阻塞。
场景5:未唤醒 - 伪唤醒导致逻辑错误(无渭词 wait)
- 描述:消费者使用无谓词
wait(),被伪唤醒后直接返回,此时任务队列为空,尝试取数据导致程序崩溃。 - 错误代码:
cpp
// 错误的消费者(无渭词wait)
void bad_consumer_no_pred() {
while (!is_finished) {
unique_lock<mutex> lock(mtx);
if (task_queue.empty()) {
cv.wait(lock); // 无渭词,伪唤醒后直接返回
}
// 伪唤醒风险:此时task_queue可能为空,front()/pop()崩溃
int task = task_queue.front();
task_queue.pop();
cout << "错误消费者(无渭词):处理任务 " << task << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
}
- 问题:伪唤醒发生时,
task_queue仍为空,task_queue.front()会触发未定义行为(程序崩溃)。
场景6:未唤醒 - 超时唤醒(wait_for 超时)
- 描述:消费者调用
wait_for等待500毫秒,生产者未生产任务,消费者超时返回,打印超时提示。 - 运行效果(基于前文
consumer_with_timeout):
plain
消费者(超时)等待500ms超时,无任务可处理
消费者(超时)等待500ms超时,无任务可处理
...
- 说明:这是预期的「未被线程唤醒,被时间唤醒」的场景,不属于错误,适用于需要超时退出的业务场景。
总结
- 条件变量是线程同步工具,需配合
mutex和共享条件使用,底层封装操作系统内核同步机制; - 核心成员函数:带谓词的
wait(推荐,防伪唤醒)、notify_one(唤醒一个)、notify_all(唤醒所有)、wait_for/wait_until(超时等待); - 关键注意事项:用
unique_lock、优先带谓词wait、共享条件受锁保护、避免丢失唤醒、notify临界区外调用; - 唤醒场景:
notify_one/notify_all唤醒等待线程,需确保线程已在等待队列中; - 未唤醒场景:丢失唤醒、唤醒无效、伪唤醒、超时等待,前三者为错误用法,需规避,超时等待为正常场景。