11. future与promise:异步任务的结果通道

多线程编程里,最让人头疼的问题是什么?我个人觉得,是「怎么把线程算好的结果拿回来」。

你想想看,你用 std::thread 开了一个线程,它吭哧吭哧算完了,结果呢?结果就丢在栈里,你主线程根本拿不到。以前我们怎么做?传引用、传指针、用全局变量……说白了,都是些「土办法」。每次写这种代码,我都觉得像是在用胶带粘水管——能用,但随时可能崩。

好在 C++11 给了我们一套正经的工具:std::futurestd::promise。这俩东西,就是专门为「异步任务的结果传递」设计的。

核心思想:promise 负责「放结果」,future 负责「取结果」。一个生产,一个消费,中间通过共享状态自动同步。

11.1 从一个小例子说起

我们先看一个最简单的用法。假设你要在后台算一个数的平方,然后把结果拿回来。

#include <iostream>
#include <future>
#include <thread>

int square(int x) {
    return x * x;
}

int main() {
    // 创建一个 promise 对象
    std::promise<int> prom;
    // 从 promise 获取 future
    std::future<int> fut = prom.get_future();

    // 启动线程,把 promise 传进去
    std::thread t([&prom]() {
        int result = square(5);
        prom.set_value(result);  // 把结果放进去
    });

    // 主线程这里可以干别的事
    std::cout << "主线程继续工作...\n";

    // 需要结果时,调用 get() 等待
    int value = fut.get();
    std::cout << "结果是: " << value << "\n";

    t.join();
    return 0;
}

这段代码里,promisefuture 就像一根管道。子线程往管子里塞结果,主线程从管子里取结果。如果主线程先调用 get(),而子线程还没算完,那主线程就会阻塞——说白了就是「等着」。直到子线程调了 set_value(),主线程才醒过来。

我的习惯:我一般把 promise 和 future 的配对叫做「一次性的通道」。一个 promise 只能 set_value 一次,一个 future 也只能 get 一次。再调就是未定义行为。

11.2 更优雅的方式:std::async

说实话,手动搞 promise 和 future 有点啰嗦。大多数时候,我们用 std::async 就够了。它把 promise 的创建、线程的启动、结果的传递全部封装好了。

#include <iostream>
#include <future>

int square(int x) {
    return x * x;
}

int main() {
    // std::async 返回一个 future
    std::future<int> fut = std::async(std::launch::async, square, 5);

    // 干点别的事
    std::cout << "主线程继续...\n";

    // 取结果
    int value = fut.get();
    std::cout << "结果是: " << value << "\n";

    return 0;
}

你看,代码简洁多了。std::async 的第一个参数是启动策略:

策略 含义
std::launch::async 强制在新线程上执行
std::launch::deferred 延迟执行,直到调用 get() 或 wait()
std::launch::async | std::launch::deferred 由系统决定(默认行为)

注意:默认策略是 async | deferred,这意味着系统可能选择延迟执行。如果你希望确保异步执行,显式指定 std::launch::async

11.3 异常处理:别让错误静默消失

我在项目中遇到过一个问题:子线程抛了异常,主线程完全不知道,程序就莫名其妙地挂了。后来排查了半天,才发现是子线程里有个除零错误。

promise 和 future 对异常的处理其实很优雅。如果子线程里抛了异常,你只需要调用 set_exception(),主线程的 get() 就会重新抛出这个异常。

#include <iostream>
#include <future>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("除零错误!");
    }
    return a / b;
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread t([&prom]() {
        try {
            int result = divide(10, 0);
            prom.set_value(result);
        } catch (...) {
            // 把异常存起来
            prom.set_exception(std::current_exception());
        }
    });

    try {
        int value = fut.get();
        std::cout << "结果是: " << value << "\n";
    } catch (const std::exception& e) {
        std::cout << "捕获异常: " << e.what() << "\n";
    }

    t.join();
    return 0;
}

std::async 就更简单了——异常会自动传递:

auto fut = std::async(std::launch::async, divide, 10, 0);
try {
    int value = fut.get();
} catch (const std::exception& e) {
    std::cout << "异常: " << e.what() << "\n";
}

避坑指南:我曾经犯过一个错——在子线程里直接抛异常,没有用 try-catch 包裹。结果 promise 被析构了,主线程的 future.get() 抛出了一个 std::future_error,错误码是 broken_promise。嗯,这个错误信息很形象——承诺被打破了。

11.4 知识体系图

下面这张图展示了 future 和 promise 的核心关系,以及异常处理的流程:

主线程(消费者) 子线程(生产者) std::promise<int> set_value(result) 或 set_exception(e) std::future<int> get() 阻塞等待,返回结果或抛异常 共享状态 存储结果或异常,自动同步 写入结果/异常 读取结果/异常 异常分支 set_exception(std::current_exception()) 异常时走虚线路径 正常结果:get() 返回 int 异常结果:get() 重新抛出异常

11.5 实战中的几个要点

最后,分享几个我在实际项目中积累的经验:

  • future 只能 get 一次。如果你需要多个地方消费同一个结果,考虑用 std::shared_future。它允许多个线程同时调用 get()。
  • promise 必须在 future 被销毁前设置值。否则 future 析构时会抛出 broken_promise 异常。我刚开始用的时候就被这个坑过。
  • 不要手动管理 promise 的生命周期。尽量用 std::asyncstd::packaged_task,它们会自动处理好这些细节。
  • 异常处理一定要完整。子线程里所有可能抛异常的地方都要 catch 住,然后用 set_exception 传递。漏掉一个,程序就可能崩。

我的建议:如果你只是简单地想异步执行一个函数并拿到结果,直接用 std::async。如果你需要更精细的控制(比如在线程池里复用线程),再考虑手动使用 promise 和 future。

好了,关于 future 和 promise 的核心用法就讲到这里。记住一句话:promise 是承诺,future 是未来。你承诺会给我一个结果,我等着那个未来。

公众号:蓝海资料掘金营,微信 deep3321