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 知识体系总览

下面这张图总结了本章的核心内容,帮你理清思路:

C++ 多线程同步机制 条件变量 std::condition_variable 等待/通知机制 必须配合 unique_lock 防止虚假唤醒 生产者-消费者 notify_one/notify_all 底层信号灯 Future/Promise 单向结果传递 promise 设置值 future 获取值 支持异常传递 不可拷贝 get() 阻塞等待 单向管道 std::async 高级封装 自动管理线程 launch::async launch::deferred 返回 future 默认策略不确定 一键异步 选择原则:简单场景用 async,需要精细控制用条件变量或 promise

嗯,到这里多线程支持的核心内容就讲完了。条件变量适合复杂的等待/通知场景,future/promise适合跨线程传递单个结果,async则是最省心的选择。我个人建议:能用async就别手写线程管理,除非你有非常特殊的性能要求。毕竟,代码越简单,越不容易出错。

一句话总结:条件变量是「手动挡」,future/promise是「自动挡」,async是「自动驾驶」——根据场景选对工具,比什么都重要。