12、多线程支持(下):条件变量、future与promise、async的使用
多线程编程里,光有互斥锁是不够的。你想想看,如果线程A要等线程B完成某个任务才能继续,总不能一直轮询吧?那太浪费CPU了。这时候就需要条件变量、future和async这些工具登场了。我个人习惯把这三者看作「线程间通信」的不同层次——条件变量是底层信号灯,future/promise是单向管道,async则是更高级的封装。
12.1 条件变量 std::condition_variable
条件变量,说白了就是让线程在某个条件满足前「睡大觉」,条件满足时再被叫醒。我在项目中遇到过这样一个场景:一个生产者线程往队列里放数据,多个消费者线程取数据。如果没有条件变量,消费者就得不停地检查队列是否为空——这叫「忙等待」,CPU都被白白烧掉了。
来看一个典型的生产者-消费者模型:
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "生产: " << i << std::endl;
}
cv.notify_one(); // 唤醒一个等待的消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !data_queue.empty(); });
// 条件满足,继续执行
int val = data_queue.front();
data_queue.pop();
lock.unlock();
std::cout << "消费: " << val << std::endl;
if (val == 9) break;
}
}
这里有个细节要注意:wait()为什么需要unique_lock而不是lock_guard?因为wait()内部会先解锁互斥量,让其他线程能访问共享数据,然后阻塞当前线程。被唤醒后又会重新加锁。这个「解锁-等待-加锁」的过程,只有unique_lock能灵活控制。
notify_one()或notify_all(),等待的线程也可能被唤醒。所以一定要用带谓词的重载形式wait(lock, predicate),或者在循环中检查条件。否则程序可能莫名其妙地崩溃。
12.2 std::future 与 std::promise
如果说条件变量是「手动挡」,那future和promise就是「自动挡」。它们提供了一种更高级的异步结果获取方式。我习惯把promise看作「承诺者」,future看作「接收者」——承诺者说「我将来会给你一个结果」,接收者就等着拿结果。
来看一个简单的例子:
#include <future>
#include <thread>
#include <iostream>
void calculate(std::promise<int> prom) {
int result = 42; // 模拟耗时计算
std::this_thread::sleep_for(std::chrono::seconds(1));
prom.set_value(result); // 兑现承诺
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(calculate, std::move(prom));
// 主线程可以干别的事...
std::cout << "等待结果..." << std::endl;
int value = fut.get(); // 阻塞直到拿到结果
std::cout << "结果: " << value << std::endl;
t.join();
return 0;
}
这里有个关键点:promise是不可拷贝的,只能用std::move传递。为什么?因为每个promise只能关联一个future,如果允许多个拷贝,那到底谁来兑现承诺?
promise::set_exception()把异常传递到future端。这样异常就能跨线程传播,非常优雅。
12.3 std::async 的高级用法
说实话,std::async是我个人最常用的多线程工具。它把线程创建、任务执行、结果获取都封装好了。你只需要告诉它「我要做什么」,它帮你搞定一切。
#include <future>
#include <iostream>
int heavy_compute(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * x;
}
int main() {
// 默认策略:可能异步,也可能同步
auto fut1 = std::async(heavy_compute, 10);
// 强制异步执行
auto fut2 = std::async(std::launch::async, heavy_compute, 20);
// 延迟执行(直到调用get()才执行)
auto fut3 = std::async(std::launch::deferred, heavy_compute, 30);
std::cout << "结果1: " << fut1.get() << std::endl;
std::cout << "结果2: " << fut2.get() << std::endl;
std::cout << "结果3: " << fut3.get() << std::endl;
return 0;
}
关于启动策略,我建议你记住这几点:
| 策略 | 行为 | 适用场景 |
|---|---|---|
std::launch::async |
立即在新线程中执行 | 需要并行加速的任务 |
std::launch::deferred |
延迟到get()时执行 |
结果可能不需要的情况 |
| 默认(不指定) | 由实现决定 | 简单场景,但行为不确定 |
async可能退化为同步执行。如果你明确需要异步,请显式指定std::launch::async。我曾经在一个高并发服务中吃过这个亏——以为所有任务都在并行,结果某些请求被串行处理了,性能直接掉了一个数量级。
12.4 知识体系总览
下面这张图总结了本章的核心内容,帮你理清思路:
嗯,到这里多线程支持的核心内容就讲完了。条件变量适合复杂的等待/通知场景,future/promise适合跨线程传递单个结果,async则是最省心的选择。我个人建议:能用async就别手写线程管理,除非你有非常特殊的性能要求。毕竟,代码越简单,越不容易出错。