一、C++11多线程thread
重点:
join和detach的使用场景
thread构造函数参数 绑定c函数 绑定类函数
线程封装基础类 互斥锁mutex
condition notify、wait lock_guard/unique_lock
function和bind
异步future/packaged_task/promise
1. 线程thread
std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。
1.1 语法
1. 默认构造函数
thread() _NOEXCEPT { _thr_set_null(_thr); }
- 功能:创建一个 “空” 的
std::thread
对象,即该对象不关联任何实际执行的线程。- 关键细节:
_NOEXCEPT
表示该构造函数 不会抛出异常,保证异常安全性。- 内部通过
_thr_set_null(_thr)
将内部线程句柄设为 “空” 状态,表明此对象未管理任何线程。- 用途:用于提前声明
std::thread
对象,后续再通过std::thread::swap
等方式关联实际线程,或作为容器元素的默认构造状态。2. 初始化构造函数
template<class Fn, class... Args> explicit thread(Fn&& fn, Args&&... args);
- 功能:创建一个
std::thread
对象,并立即启动一个新线程。新线程执行函数fn
,并将args
作为参数传递给fn
。- 关键细节:
explicit
关键字禁止隐式类型转换,必须显式构造(如std::thread t(fn, args);
不能省略std::thread
)。- 利用 完美转发(
Args&&... args
),确保参数按原样传递给新线程的函数,避免不必要的拷贝或类型退化。- 新创建的
std::thread
对象处于 可 joinable 状态(即关联了实际线程),后续需通过join()
或detach()
管理线程生命周期。- 用途:直接启动新线程执行特定任务,如
std::thread t([]{ /* 线程代码 */ });
。3. 拷贝构造函数(禁用)
thread(const thread&) = delete;
- 功能:明确禁用
std::thread
的拷贝构造。- 关键细节:
- 若尝试拷贝
std::thread
(如std::thread t2 = t1;
),编译器会报错。这是因为每个std::thread
对象代表唯一的执行线程,拷贝会导致多个对象管理同一线程,引发资源竞争和逻辑混乱(如join()
多次调用)。- 替代方案:若需转移线程所有权,使用 移动语义(
std::thread t2 = std::move(t1);
),移动后t1
变为空状态(等同于默认构造)。
1.2 move构造函数
Move 构造函数
- 定义:
thread(thread&& x) noexcept
- 作用:转移线程所有权。调用后,原对象
x
不再关联任何线程执行对象,新对象获得线程所有权。- 注意:若线程处于
joinable
状态(即关联有效线程),必须在其销毁前调用join()
(等待线程结束)或detach()
(分离线程,使其后台运行),否则程序终止时会报错。
- 示例:
#include <iostream> #include <thread> #include <chrono> void thread_task(int& a) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作 a = 0; // 修改引用参数 std::cout << "子线程执行完毕,a 被修改为 0" << std::endl; } int main() { int x = 10; // 创建线程 t1,执行 thread_task(传递 x 的引用) std::thread t1(thread_task, std::ref(x)); // 转移线程所有权到 t2(移动构造) std::thread t2 = std::move(t1); // 此时 t1 不再关联任何线程(调用 t1.join() 会崩溃!) if (t1.joinable()) { std::cout << "t1 仍关联线程(错误状态)" << std::endl; } else { std::cout << "t1 已无关联线程(正确状态)" << std::endl; } // t2 现在持有线程所有权,必须管理其生命周期(join 或 detach) t2.join(); std::cout << "主线程:x = " << x << std::endl; // 输出 x=0(子线程修改成功) return 0; }
示例代码中,t1
创建后通过std::move
将所有权转移给t2
,t2
再转移给t3
。若此时调用t1.join()
会出错,因为t1
已无关联线程。线程函数threadFun
修改了引用参数a
(即x
),最终x
由10
变为0
。主要成员函数
get_id()
- 功能:获取线程的唯一标识
id
,返回类型为thread::id
。- 用途:用于识别和区分不同线程,例如日志记录或调试时标记线程。
#include <iostream> #include <thread> #include <chrono> void worker() { std::cout << "子线程 ID: " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } int main() { std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl; std::thread t(worker); std::cout << "t 关联的子线程 ID: " << t.get_id() << std::endl; // 与 worker 中输出一致 t.join(); // 线程结束后,t.get_id() 返回 "无效 ID"(具体值因平台而异) std::cout << "t 结束后 ID: " << t.get_id() << std::endl; return 0; }
joinable()
- 功能:判断线程是否可加入(即是否关联有效线程且未被
join
或detach
)。- 返回值:若线程可加入(处于有效执行状态),返回
true
;否则返回false
。#include <iostream> #include <thread> #include <chrono> void task() { std::this_thread::sleep_for(std::chrono::seconds(1)); } int main() { std::thread t1; // 默认构造的线程(未关联任何线程) std::cout << "t1 joinable? " << std::boolalpha << t1.joinable() << std::endl; // 输出 false std::thread t2(task); std::cout << "t2 启动后 joinable? " << t2.joinable() << std::endl; // 输出 true t2.join(); std::cout << "t2 join 后 joinable? " << t2.joinable() << std::endl; // 输出 false(资源已回收) std::thread t3(task); t3.detach(); std::cout << "t3 detach 后 joinable? " << t3.joinable() << std::endl; // 输出 false(线程已分离) return 0; }
join()
- 功能:阻塞当前线程,直到关联线程执行完毕。之后,当前线程回收关联线程的资源,
joinable()
变为false
。- 场景:需要等待子线程完成任务后再继续主线程逻辑时使用。
#include <iostream> #include <thread> #include <vector> // 子线程任务:计算 1~n 的和 void sum_task(int n, int& result) { result = 0; for (int i = 1; i <= n; ++i) { result += i; std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟计算耗时 } } int main() { int sum = 0; std::thread t(sum_task, 10, std::ref(sum)); std::cout << "主线程:等待子线程计算结果..." << std::endl; t.join(); // 阻塞主线程,直到子线程完成 std::cout << "主线程:计算结果为 " << sum << std::endl; // 输出 55 return 0; }
detach()
- 功能:分离线程,使其在后台独立运行,与
std::thread
对象失去关联。运行时库负责清理其资源,调用后joinable()
变为false
,get_id()
也不再代表原线程。- 场景:无需等待子线程完成,且希望其后台运行时使用(如异步日志线程)。
#include <iostream> #include <thread> #include <chrono> // 后台日志线程:持续输出日志(模拟) void log_task() { for (int i = 0; i < 5; ++i) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "日志线程:记录第 " << i+1 << " 条日志" << std::endl; } } int main() { std::thread t(log_task); t.detach(); // 分离线程,使其后台运行 std::cout << "主线程:启动日志线程后继续执行..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(3)); // 主线程等待 3 秒(避免提前退出) std::cout << "主线程:退出" << std::endl; return 0; }
2 . 互斥量
mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件中,所以如果 你需要使用 std::mutex,就必须包含 头文件。
C++11提供如下4种语义的互斥量(mutex)
std::mutex,独占的互斥量,不能递归使用。
std::time_mutex,带超时的独占互斥量,不能递归使用。
std::recursive_mutex,递归互斥量,不带超时功能。
std::recursive_timed_mutex,带超时的递归互斥量。
2.1 独占互斥量 std::mutex
std::mutex
介绍下面以
std::mutex
为例介绍 C++11 中的互斥量用法。std::mutex
是 C++11 中最基本的互斥量,std::mutex
对象提供了独占所有权的特性 —— 即不支持递归地对std::mutex
对象上锁,而std::recursive_lock
则可以递归地对互斥量对象上锁。
std::mutex
的成员函数
- 构造函数:
std::mutex
不允许拷贝构造,也不允许 move 拷贝,最初产生的mutex
对象处于unlocked
状态。lock()
:调用线程将锁住该互斥量。线程调用该函数会发生以下 3 种情况:
- 如果该互斥量当前没有被锁住,则调用线程将其锁住,直到调用
unlock
前,该线程一直拥有该锁。- 如果当前互斥量被其他线程锁住,当前调用线程被阻塞。
- 如果当前互斥量被当前调用线程锁住,会产生死锁(deadlock)。
unlock()
:解锁,释放对互斥量的所有权。try_lock()
:尝试锁住互斥量,若互斥量被其他线程占有,当前线程不会被阻塞。线程调用该函数会出现以下 3 种情况:
- 如果当前互斥量未被其他线程占有,该线程锁住互斥量,直到调用
unlock
释放。- 如果当前互斥量被其他线程锁住,当前调用线程返回
false
,且不会被阻塞。- 如果当前互斥量被当前调用线程锁住,会产生死锁(deadlock)。
#include <iostream> #include <thread> #include <mutex> #include <chrono> std::mutex mtx; // 线程函数:尝试获取锁,成功则操作,否则跳过 void try_lock_task() { for (int i = 0; i < 3; ++i) { // 尝试加锁(非阻塞,立即返回结果) if (mtx.try_lock()) { std::cout << "线程 " << std::this_thread::get_id() << " 获取锁,执行操作" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟操作耗时 mtx.unlock(); // 手动解锁 } else { std::cout << "线程 " << std::this_thread::get_id() << " 未获取锁,跳过操作" << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(300)); // 间隔一段时间重试 } } int main() { std::thread t1(try_lock_task); std::thread t2(try_lock_task); t1.join(); t2.join(); return 0; }
2.2 递归互斥量std::recursive_mutex
递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
//死锁范例1-2-mutex2-dead-lock #include <iostream> #include <thread> #include <mutex> struct Complex { std::mutex mutex; int i; Complex() : i(0) {} void mul(int x) { std::lock_guard<std::mutex> lock(mutex); i *= x; } void div(int x) { std::lock_guard<std::mutex> lock(mutex); i /= x; } void both(int x, int y) { std::lock_guard<std::mutex> lock(mutex); mul(x); div(y); } }; int main(void) { Complex complex; complex.both(32, 23); return 0; }
运行后出现死锁情况。在调用
both
时获取了互斥量,在调用mul
时又要获取互斥量,但both
并未释放,从而产生死锁。//范例1-2-recursive_mutex1 #include <iostream> #include <thread> #include <mutex> struct Complex { std::recursive_mutex mutex; int i; Complex() : i(0) {} void mul(int x) { std::lock_guard<std::recursive_mutex> lock(mutex); i *= x; } void div(int x) { std::lock_guard<std::recursive_mutex> lock(mutex); i /= x; } void both(int x, int y) { std::lock_guard<std::recursive_mutex> lock(mutex); mul(x); div(y); } }; int main(void) { Complex complex; complex.both(32, 23); //因为同一线程可以多次获取同一互斥量,不会发生死锁 std::cout << "main finish\n"; return 0; }
虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:
- 需要用到递归锁的多线程代码本身是可以简化的,允许递归很容易放纵复杂逻辑的产生,并产生漏洞,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁;
- 递归锁比起非递归锁,效率会低;
- 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定次数就会抛出
std::system
错误。
2.3 带超时的互斥量std::timed_mutex和 std::recursive_timed_mutex
std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until
//1-2-timed_mutex
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex mutex;
void work()
{
std::chrono::milliseconds timeout(100);
while (true)
{
if (mutex.try_lock_for(timeout))
{
std::cout << std::this_thread::get_id() << ": do work with the mutex" << std::endl;
std::chrono::milliseconds sleepDuration(250);
std::this_thread::sleep_for(sleepDuration);
mutex.unlock();
std::this_thread::sleep_for(sleepDuration);
}
else
{
std::cout << std::this_thread::get_id() << ": do work without the mutex" << std::endl;
std::chrono::milliseconds sleepDuration(100);
std::this_thread::sleep_for(sleepDuration);
}
}
}
int main(void)
{
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
std::cout << "main finish\n";
return 0;
}
3. lock_guard和unique_lock的使用和区别
相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式。 RAII:也称为“资源获取就是初始化”,是 c++等编程语言常用的管理资源、避免内存泄露的方法。它保证 在任何情况下,使用对象时先构造对象,最后析构对象。
3.1 unique_lock,lock_guard的使用
这里涉及到unique_lock,lock_guard的使用。
一、核心区别概览
特性 lock_guard
unique_lock
锁管理方式 严格基于作用域的自动管理(构造时加锁,析构时解锁) 支持手动控制加锁 / 解锁( lock()
/unlock()
),支持延迟加锁、超时加锁等灵活性 功能单一,仅适用于简单临界区保护 功能丰富,可配合条件变量( std::condition_variable
)使用移动语义 不支持移动( move
)支持移动(但不可复制) 适用场景 简单互斥场景(如短时间保护共享数据) 复杂同步场景(如条件变量等待、锁的分阶段操作) 二、
lock_guard
:轻量级锁管理
lock_guard
是最基础的锁管理类,其行为非常 “保守”:构造时立即尝试加锁(若互斥锁已被其他线程持有则阻塞),析构时自动解锁。适合保护短时间的临界区代码。代码案例:多线程计数
假设有一个共享的计数器
g_count
,多个线程需要安全地递增它。使用lock_guard
可以确保每次递增操作的原子性。#include <iostream> #include <mutex> #include <thread> int g_count = 0; // 共享计数器 std::mutex g_mutex; // 保护计数器的互斥锁 void increment(int times) { for (int i = 0; i < times; ++i) { std::lock_guard<std::mutex> lock(g_mutex); // 构造时加锁,析构时解锁 g_count++; // 临界区:仅当前线程可访问 } } int main() { const int kTimes = 10000; std::thread t1(increment, kTimes); std::thread t2(increment, kTimes); t1.join(); t2.join(); std::cout << "最终计数: " << g_count << std::endl; // 输出 20000(正确) return 0; }
关键点说明
std::lock_guard<std::mutex> lock(g_mutex);
:构造lock_guard
对象时,会调用g_mutex.lock()
,若锁被其他线程持有则阻塞等待。- 当
lock
离开作用域(如for
循环的每次迭代结束)时,lock_guard
析构,自动调用g_mutex.unlock()
释放锁。- 无需手动调用
lock()
或unlock()
,避免了因忘记解锁导致的死锁。三、
unique_lock
:灵活的锁管理
unique_lock
是lock_guard
的 “增强版”,支持更复杂的锁操作:
- 延迟加锁:构造时不立即加锁(通过
std::defer_lock
参数),可在需要时手动加锁。- 超时加锁:尝试加锁时设置超时时间(
try_lock_for
/try_lock_until
)。- 配合条件变量:条件变量(
std::condition_variable
)的wait()
方法要求使用unique_lock
,因为它需要在等待期间自动解锁和重新加锁。代码案例:条件变量等待任务
假设有一个 “生产者 - 消费者” 场景:消费者线程等待生产者生成数据后再处理。此时需要用
unique_lock
配合条件变量实现同步。#include <iostream> #include <mutex> #include <thread> #include <condition_variable> bool g_data_ready = false; // 数据就绪标志 std::mutex g_mutex; std::condition_variable g_cv; // 条件变量 void producer() { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟生产耗时 { std::unique_lock<std::mutex> lock(g_mutex); // 构造时加锁(默认行为) g_data_ready = true; std::cout << "生产者:数据已生成" << std::endl; } // 离开作用域前手动解锁(也可提前解锁) g_cv.notify_one(); // 通知等待的消费者 } void consumer() { std::unique_lock<std::mutex> lock(g_mutex); // 构造时加锁 // 等待条件变量通知(等待期间自动解锁,被唤醒后重新加锁) g_cv.wait(lock, []{ return g_data_ready; }); // 第二个参数是谓词,避免虚假唤醒 std::cout << "消费者:处理数据" << std::endl; } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); return 0; }
关键点说明
std::unique_lock<std::mutex> lock(g_mutex);
:默认构造时加锁,与lock_guard
类似。g_cv.wait(lock, 谓词)
:wait
会自动释放锁(lock.unlock()
),使其他线程(如生产者)可以获取锁并修改g_data_ready
。当被notify_one()
唤醒时,会重新加锁(lock.lock()
)并检查谓词(若为false
则继续等待)。- 可通过
lock.unlock()
手动解锁,或通过lock.try_lock_for(超时时间)
尝试非阻塞加锁(适合需要限时等待的场景)。四、总结
- 选
lock_guard
:当需要简单、轻量的锁管理,且临界区明确(如短代码块)时使用。- 选
unique_lock
:当需要延迟加锁、超时加锁,或配合条件变量实现复杂同步时使用(虽然性能略低于lock_guard
,但功能更强大)。
4. 条件变量
C++ 中的条件变量(std::condition_variable
)是多线程同步的核心工具之一,用于协调多个线程之间的执行顺序,解决 “等待某个条件满足” 的场景。它需要与互斥锁(通常是std::unique_lock
)配合使用,通过 “等待 - 通知” 机制实现高效的线程协作。
一、条件变量的核心接口
条件变量的核心接口包括以下 5 个方法,覆盖了 “等待条件” 和 “通知条件” 两类操作:
接口 功能描述 wait(lock)
阻塞当前线程,直到被其他线程通过 notify_one
/notify_all
唤醒。必须配合unique_lock
使用。wait(lock, pred)
等价于 while (!pred()) wait(lock);
,避免虚假唤醒(Spurious Wakeups)。wait_for(lock, rel_time)
阻塞当前线程,等待最多 rel_time
时间(如std::chrono::seconds(2)
),超时或被唤醒时返回。wait_until(lock, abs_time)
阻塞当前线程,等待到绝对时间点 abs_time
(如std::chrono::steady_clock::now() + 2s
),超时或被唤醒时返回。notify_one()
唤醒一个等待该条件变量的线程(若有多个,选择不确定)。 notify_all()
唤醒所有等待该条件变量的线程。 二、关键使用规则
- 必须与
std::unique_lock
配合:条件变量的wait
系列函数需要在阻塞时自动释放锁(调用lock.unlock()
),并在被唤醒时重新加锁(调用lock.lock()
)。std::lock_guard
不支持手动解锁,因此无法配合条件变量使用。- 共享状态需用互斥锁保护:条件等待的 “条件”(如某个标志位)必须由互斥锁保护,确保修改条件时的原子性。
- 避免虚假唤醒:即使没有收到通知,
wait
也可能因系统原因被唤醒(概率极低但存在)。因此,必须通过 ** 谓词(pred
)** 检查条件是否真的满足。三、代码案例演示
以下通过 3 个典型场景,演示条件变量的核心接口和使用方法。
案例 1:基础生产者 - 消费者模型(
wait
与notify_one
)场景:生产者线程生成数据,消费者线程等待数据就绪后处理。
核心逻辑:消费者通过wait
等待数据就绪,生产者生成数据后通过notify_one
唤醒消费者。#include <iostream> #include <mutex> #include <thread> #include <condition_variable> #include <queue> std::queue<int> g_data_queue; // 共享数据队列 std::mutex g_mutex; // 保护队列的互斥锁 std::condition_variable g_cv; // 条件变量:通知数据就绪 // 生产者:生成数据并放入队列 void producer(int data_count) { for (int i = 0; i < data_count; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时 { std::unique_lock<std::mutex> lock(g_mutex); // 加锁保护共享状态 g_data_queue.push(i); std::cout << "生产者:生成数据 " << i << std::endl; } // 离开作用域前自动解锁(也可提前解锁) g_cv.notify_one(); // 通知一个消费者线程 } } // 消费者:等待数据并处理 void consumer() { while (true) { std::unique_lock<std::mutex> lock(g_mutex); // 加锁保护共享状态 // 等待条件变量通知,且队列非空(避免虚假唤醒) g_cv.wait(lock, []{ return !g_data_queue.empty(); }); // 处理数据 int data = g_data_queue.front(); g_data_queue.pop(); std::cout << "消费者:处理数据 " << data << std::endl; // 若数据已全部处理,退出循环(假设生产者生成10个数据) if (data == 9) break; } } int main() { std::thread prod(producer, 10); // 生产10个数据 std::thread cons(consumer); prod.join(); cons.join(); return 0; }
关键点说明:
g_cv.wait(lock, []{ return !g_data_queue.empty(); })
:消费者线程在此阻塞,直到队列非空(通过谓词检查)。wait
会自动释放锁,允许生产者获取锁并修改队列。- 生产者生成数据后调用
notify_one()
,唤醒一个消费者线程。- 必须用
unique_lock
而非lock_guard
,因为wait
需要在阻塞时释放锁,并在唤醒时重新加锁。案例 2:超时等待(
wait_for
)场景:消费者等待数据时设置超时时间,若超时则执行备用逻辑(如报错或重试)。
核心逻辑:使用wait_for
替代wait
,并检查返回值是否超时。#include <iostream> #include <mutex> #include <thread> #include <condition_variable> #include <chrono> bool g_data_ready = false; // 数据就绪标志 std::mutex g_mutex; std::condition_variable g_cv; // 生产者:2秒后标记数据就绪 void producer() { std::this_thread::sleep_for(std::chrono::seconds(2)); { std::lock_guard<std::mutex> lock(g_mutex); g_data_ready = true; } g_cv.notify_one(); } // 消费者:最多等待1.5秒,超时则报错 void consumer() { std::unique_lock<std::mutex> lock(g_mutex); // 等待1.5秒,或直到g_data_ready为true auto status = g_cv.wait_for(lock, std::chrono::milliseconds(1500), []{ return g_data_ready; }); if (status) { // 被唤醒且条件满足 std::cout << "消费者:数据就绪,开始处理" << std::endl; } else { // 超时 std::cout << "消费者:等待超时,数据未就绪" << std::endl; } } int main() { std::thread prod(producer); std::thread cons(consumer); prod.join(); cons.join(); return 0; }
关键点说明:
wait_for
的返回值是bool
(若传入谓词)或std::cv_status
(若未传谓词)。本例中传入谓词,返回true
表示条件满足,false
表示超时。- 生产者在 2 秒后标记数据就绪,但消费者仅等待 1.5 秒,因此会触发超时逻辑。
- 超时场景适用于需要限制等待时间的情况(如网络请求、用户输入响应)。
案例 3:多线程唤醒(
notify_all
)场景:多个消费者线程等待同一条件,生产者完成任务后唤醒所有消费者(如初始化完成后多个线程开始工作)。
核心逻辑:使用notify_all
替代notify_one
,唤醒所有等待线程。#include <iostream> #include <mutex> #include <thread> #include <condition_variable> #include <vector> bool g_initialized = false; // 初始化完成标志 std::mutex g_mutex; std::condition_variable g_cv; // 工作线程:等待初始化完成后执行任务 void worker(int id) { std::unique_lock<std::mutex> lock(g_mutex); g_cv.wait(lock, []{ return g_initialized; }); // 等待初始化完成 std::cout << "工作线程 " << id << ":开始执行任务" << std::endl; } // 初始化线程:模拟耗时初始化,完成后通知所有工作线程 void initializer() { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟初始化耗时 { std::lock_guard<std::mutex> lock(g_mutex); g_initialized = true; } g_cv.notify_all(); // 唤醒所有等待的工作线程 } int main() { const int kWorkerCount = 3; std::vector<std::thread> workers; // 创建3个工作线程 for (int i = 0; i < kWorkerCount; ++i) { workers.emplace_back(worker, i); } // 创建初始化线程 std::thread init(initializer); // 等待所有线程完成 init.join(); for (auto& t : workers) { t.join(); } return 0; }
关键点说明:
notify_all()
会唤醒所有等待g_cv
的线程(本例中 3 个工作线程),每个线程被唤醒后都会检查谓词(g_initialized
是否为true
)。- 若使用
notify_one()
,仅会唤醒一个线程,其他线程会继续阻塞,直到再次被通知。- 多线程唤醒适用于 “一个条件变化需要多个线程响应” 的场景(如配置加载完成后所有模块启动)。
四、总结
条件变量是 C++ 多线程同步的核心工具,其接口设计围绕 “等待条件” 和 “通知条件” 展开:
wait
系列:用于阻塞线程并等待条件满足,支持谓词检查避免虚假唤醒,wait_for
/wait_until
支持超时控制。notify
系列:notify_one
唤醒单个线程(适合单消费者场景),notify_all
唤醒所有线程(适合多消费者场景)。
二、原子变量
1、原子变量(std::atomic)
原子变量是 C++11 引入的线程安全工具,用于保证多线程对共享变量的无锁原子操作(无需互斥锁),避免竞态条件(Race Condition)。
1.1 核心特性
- 无锁操作:通过 CPU 原子指令(如
lock cmpxchg
)保证操作的原子性,比互斥锁更轻量。 - 线程安全:对原子变量的读 / 写操作(如
load
/store
)是原子的,无需额外同步。 - 内存序控制:通过
std::memory_order
参数控制内存访问的可见性和顺序(如memory_order_relaxed
仅保证原子性,不限制内存顺序)。
1.2 关键接口
接口 | 功能描述 |
---|---|
atomic<T> var(val) | 构造原子变量并初始化为val (注意:不能用= 直接赋值初始化,如atomic<int> count=0 错误)。 |
store(val, order) | 原子性地将值val 写入原子变量(写操作),order 指定内存序。 |
load(order) | 原子性地读取原子变量的值(读操作),order 指定内存序。 |
exchange(val, order) | 原子性地替换原子变量的值,并返回旧值。 |
compare_exchange_weak/strong | 原子性地比较并交换值(CAS 操作),用于实现无锁数据结构。 |
1.3 代码案例:原子变量的基本使用
以下代码演示两个线程通过原子变量同步:一个线程写入值,另一个线程等待值非零后打印。
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> count(0); // 正确初始化:通过构造函数赋值
// 写入线程:设置原子变量的值
void set_count(int x) {
std::cout << "set_count: " << x << std::endl;
count.store(x, std::memory_order_relaxed); // 原子写(宽松内存序)
}
// 读取线程:等待值非零后打印
void print_count() {
int x;
do {
x = count.load(std::memory_order_relaxed); // 原子读(宽松内存序)
} while (x == 0); // 等待值被设置
std::cout << "count: " << x << std::endl;
}
int main() {
std::thread t1(print_count); // 启动读取线程
std::thread t2(set_count, 10); // 启动写入线程(传递参数10)
t1.join();
t2.join();
std::cout << "main finish" << std::endl;
return 0;
}
关键点说明:
- 原子变量
count
通过构造函数初始化(atomic<int> count(0)
),不能用=
直接赋值(atomic<int> count=0
会编译错误,因为原子变量的拷贝构造被删除)。 store
和load
使用memory_order_relaxed
(宽松内存序),仅保证操作的原子性,不限制线程间的内存可见顺序(适合对顺序不敏感的场景,如计数器)。- 读取线程通过循环
load
等待写入线程store
值,避免了互斥锁的开销。
2、异步操作(std::future 与 std::async)
C++11 引入的异步操作库(<future>
头文件)用于简化多线程编程,核心组件包括std::future
、std::async
、std::packaged_task
和std::promise
,其中std::async
和std::future
是最常用的组合。
2.1 核心概念
- std::future:表示一个 “未来” 的结果,用于获取异步任务的返回值。它是一次性的,只能通过
get()
获取一次结果(之后future
变为无效)。 - std::async:启动一个异步任务,返回一个
std::future
对象,用于获取任务结果。支持两种启动策略:std::launch::async
:强制在新线程中运行任务。std::launch::deferred
:延迟运行任务(直到调用future.get()
或wait()
时才执行)。
- 默认策略:
std::async
默认使用std::launch::any
(即可能异步运行或延迟运行,由编译器决定)。
2.2 关键接口
接口 / 方法 | 功能描述 |
---|---|
std::async(fun, args...) | 启动异步任务fun ,传递参数args ,返回std::future<返回类型> 。 |
future.get() | 阻塞当前线程,直到异步任务完成并返回结果(调用后future 失效)。 |
future.wait() | 阻塞当前线程,直到异步任务完成(不获取结果)。 |
future.wait_for(rel_time) | 阻塞最多rel_time 时间,返回future_status::ready (就绪)或timeout (超时)。 |
2.3 代码案例:std::async 与 std::future 的配合使用
以下代码演示通过std::async
启动异步任务,主线程执行其他操作后获取结果。
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 异步任务1:计算1+1(模拟2秒延迟)
int find_result_to_add() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "find_result_to_add 执行完成" << std::endl;
return 1 + 1;
}
// 异步任务2:计算a+b(模拟5秒延迟)
int find_result_to_add2(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "find_result_to_add2 执行完成" << std::endl;
return a + b;
}
// 主线程任务:模拟其他操作
void do_other_things() {
std::cout << "开始执行其他任务..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟耗时3秒
std::cout << "其他任务执行完成" << std::endl;
}
int main() {
// 启动异步任务1(自动推导future类型)
auto result1 = std::async(std::launch::async, find_result_to_add);
// 启动异步任务2(传递参数10和20)
auto result2 = std::async(std::launch::async, find_result_to_add2, 10, 20);
// 主线程执行其他任务(不阻塞)
do_other_things();
// 获取异步结果(阻塞直到任务完成)
std::cout << "result1: " << result1.get() << std::endl; // 等待2秒后输出2
std::cout << "result2: " << result2.get() << std::endl; // 等待5秒后输出30
std::cout << "main finish" << std::endl;
return 0;
}
关键点说明:
std::async(std::launch::async, fun, args...)
:显式指定std::launch::async
策略,强制在新线程中运行任务(避免默认策略可能的延迟执行)。future.get()
:主线程调用get()
时会阻塞,直到对应异步任务完成。例如,result1.get()
等待 2 秒(任务 1 的延迟),result2.get()
等待 5 秒(任务 2 的延迟)。- 主线程的
do_other_things()
与异步任务并行执行(总耗时约 5 秒,而非 2+3+5=10 秒)。
3、std::packaged_task:任务与 future 的绑定
std::packaged_task
是 C++11 引入的模板类,用于将 ** 可调用对象(函数、lambda 等)** 与std::future
绑定,允许异步执行任务并通过future
获取结果。其核心作用是将任务封装为一个可调用对象,便于在线程间传递或延迟执行。
3.1 核心特性
- 任务封装:将可调用对象(如函数、lambda)封装为
packaged_task
对象,支持延迟执行或异步执行。 - 结果同步:任务执行后的返回值或异常会存储在关联的
std::future
中,其他线程可通过future
获取结果。 - 灵活调用:可通过直接调用(如
task(args...)
)或在线程中执行(如std::thread(t, std::move(task))
)触发任务。
3.2 关键接口
接口 / 方法 | 功能描述 |
---|---|
packaged_task<Signature> task(callable) | 构造packaged_task 对象,Signature 是任务的函数签名(如int(int, int) ),callable 是要封装的可调用对象。 |
task.get_future() | 返回与任务关联的std::future 对象(类型由Signature 的返回值决定)。 |
task(args...) | 调用任务,传入参数args ,执行可调用对象并将结果存储到关联的future 中。 |
task.valid() | 检查packaged_task 是否关联有效任务(未被移动或调用过)。 |
3.3 代码案例:packaged_task 的基本使用
以下代码演示通过packaged_task
封装任务,主线程执行其他操作后通过future
获取结果。
#include <iostream>
#include <future>
#include <thread>
// 被封装的任务函数:计算三个数的和
int add(int a, int b, int c) {
std::cout << "任务执行:计算 " << a << "+" << b << "+" << c << std::endl;
return a + b + c;
}
// 主线程的其他操作
void do_other_things() {
std::cout << "执行其他任务..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
}
int main() {
// 步骤1:封装任务(函数签名为 int(int, int, int))
std::packaged_task<int(int, int, int)> task(add);
// 步骤2:获取关联的future(用于获取结果)
std::future<int> result = task.get_future();
// 步骤3:执行其他操作(不阻塞)
do_other_things();
// 步骤4:执行任务(传入参数1, 1, 2)
task(1, 1, 2); // 调用后,结果会存储到result中
// 步骤5:获取任务结果(阻塞直到任务完成)
std::cout << "任务结果: " << result.get() << std::endl; // 输出4
return 0;
}
关键点说明:
std::packaged_task<int(int, int, int)>
的模板参数是任务的函数签名(返回值 + 参数类型),与add
函数的类型严格匹配。task.get_future()
必须在任务执行前调用,否则会因task
失效而抛出异常。- 任务通过
task(1, 1, 2)
直接调用执行(也可通过线程异步执行,如std::thread(std::move(task), 1, 1, 2)
)。
4、std::promise:手动设置 future 的结果
std::promise
是 C++11 引入的模板类,用于手动设置一个值或异常,并通过关联的std::future
传递给其他线程。它解决了 “无法通过函数返回值传递结果” 的场景(如跨线程通知)。
4.1 核心特性
- 结果传递:通过
promise.set_value(val)
设置结果,关联的future
会变为就绪状态,其他线程可通过future.get()
获取该值。 - 异常传递:通过
promise.set_exception(ex)
设置异常,future.get()
会抛出该异常。 - 线程协作:适合一个线程负责计算结果,另一个线程等待结果的场景(如主 - 从线程同步)。
4.2 关键接口
接口 / 方法 | 功能描述 |
---|---|
promise<Type> p | 构造promise 对象,Type 是要设置的值的类型。 |
p.get_future() | 返回与promise 关联的std::future<Type> 对象(只能调用一次)。 |
p.set_value(val) | 设置结果值val ,关联的future 变为就绪(若已设置过会抛出异常)。 |
p.set_exception(ex) | 设置异常ex (需通过std::make_exception_ptr 包装),关联的future 变为就绪。 |
4.3 代码案例:promise 的跨线程结果传递
以下代码演示两个线程通过promise
和future
协作:一个线程设置结果,另一个线程等待并读取结果。
#include <iostream>
#include <future>
#include <thread>
#include <stdexcept> // 用于异常
// 线程函数:设置promise的结果
void set_promise_value(std::promise<std::string>& p) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟计算耗时
p.set_value("计算完成!结果是42"); // 设置结果
}
// 线程函数:设置promise的异常(示例)
void set_promise_exception(std::promise<int>& p) {
try {
throw std::runtime_error("模拟一个错误"); // 抛出异常
} catch (...) {
p.set_exception(std::current_exception()); // 捕获并设置异常
}
}
// 主线程操作
void do_other_things() {
std::cout << "主线程执行其他操作..." << std::endl;
}
int main() {
// 案例1:正常设置值
{
std::promise<std::string> p;
std::future<std::string> result = p.get_future();
std::thread t(set_promise_value, std::ref(p)); // 传递promise的引用(需用std::ref)
do_other_things(); // 主线程执行其他操作
std::cout << "获取结果: " << result.get() << std::endl; // 等待2秒后输出结果
t.join();
}
// 案例2:设置异常
{
std::promise<int> p;
std::future<int> result = p.get_future();
std::thread t(set_promise_exception, std::ref(p));
t.join(); // 等待线程完成异常设置
try {
result.get(); // 尝试获取结果(会抛出异常)
} catch (const std::runtime_error& e) {
std::cout << "捕获异常: " << e.what() << std::endl; // 输出"模拟一个错误"
}
}
return 0;
}
关键点说明:
std::promise
需通过std::ref
传递给线程(因为promise
不可拷贝,只能移动或传递引用)。set_value
和set_exception
只能调用一次,重复调用会抛出std::future_error
。- 通过
future.get()
获取结果时,若promise
设置了异常,get()
会重新抛出该异常(需用try-catch
捕获)。
5、什么叫无法通过函数返回值传递结果的场景
在编程中,“无法通过函数返回值传递结果的场景”通常指函数调用者与被调用者不在同一执行线程或同一执行流程中,导致无法直接通过函数的
return
语句传递结果的情况。以下是具体分析和示例:一、为什么函数返回值会 “失效”?
1. 异步编程场景(多线程 / 异步操作)
- 核心问题:当函数在另一个线程或异步上下文中执行时,调用者无法通过同步的
return
语句获取结果。- 示例:
// 子线程函数(无返回值,无法通过 return 传递结果) void worker() { int result = complex_calculation(); // 计算结果 // 无法直接将 result 返回给主线程 } int main() { std::thread t(worker); // 启动子线程 t.join(); // 如何获取 worker 中的 result?无法通过 return! return 0; }
- 分析:子线程的
worker
函数没有返回值,且主线程无法阻塞等待其返回(即使有返回值,线程函数的返回值也无法被主线程直接捕获)。2. 回调函数场景(非阻塞调用)
- 核心问题:函数通过回调机制异步通知结果,而非通过返回值传递。
- 示例(伪代码):
// 异步加载文件,结果通过回调函数处理 void load_file_async(const std::string& path, std::function<void(const std::string&)> callback) { // 异步读取文件(如通过 IO 线程) // 读取完成后,通过回调传递结果,而非 return callback(content); } int main() { load_file_async("data.txt", [](const std::string& content) { std::cout << "文件内容:" << content << std::endl; }); // 无法通过 return 获取 content,必须通过回调 return 0; }
- 分析:调用者通过回调函数接收结果,而非函数返回值。
3. 跨作用域 / 跨生命周期传递结果
- 核心问题:函数执行结果需要在更长的生命周期或不同作用域中使用,无法通过短期的函数调用链传递。
- 示例:
- 网络请求:发起请求后,结果可能在数秒后返回,调用者无法阻塞等待
return
。- 事件驱动程序:结果通过事件循环异步触发,而非函数调用返回。
二、如何解决 “无法通过返回值传递结果” 的问题?
C++ 的
<future>
库(std::promise
、std::future
、std::packaged_task
)正是为解决这类问题设计的,核心思路是:
- 通过共享状态(shared state)解耦任务执行与结果获取:
std::promise
:允许手动设置结果值,关联的std::future
可异步获取该值。std::packaged_task
:封装可调用对象,将其返回值自动存储到std::future
中。示例:用
std::promise
跨线程传递结果#include <future> #include <thread> #include <iostream> void worker(std::promise<int>& p) { // 通过 promise 设置结果 int result = 42; p.set_value(result); // 将结果存入 promise 的共享状态 } int main() { std::promise<int> prom; // 创建 promise std::future<int> fut = prom.get_future(); // 获取关联的 future std::thread t(worker, std::ref(prom)); // 子线程通过 promise 设置结果 t.join(); int value = fut.get(); // 通过 future 异步获取结果(阻塞直到结果就绪) std::cout << "结果:" << value << std::endl; // 输出:42 return 0; }
- 关键点:子线程通过
promise
设置结果,主线程通过future
获取结果,无需依赖函数返回值。三、对比:函数返回值 vs.
std::future
场景 函数返回值 std::future
/std::promise
同步调用 直接有效(调用者阻塞等待结果) 无需使用(杀鸡用牛刀) 异步调用(多线程) 无效(线程函数无返回值或无法捕获) 有效(通过共享状态跨线程传递结果) 非阻塞回调场景 无效(结果通过回调传递) 有效(可将回调转换为 future 模式)