异步编程(Future与Promise):std::async、std::future与std::promise、std::packaged_task

聊到异步编程,很多人的第一反应就是「开个线程跑任务」。嗯,这话没错,但不够优雅。你想想看,如果每次异步操作都要手动管理线程、传递结果、处理异常,那代码很快就会变成一团乱麻。

我个人习惯用 std::futurestd::promise 这套机制。它们把「异步任务的发起」和「结果的获取」解耦了。说白了,你只管把任务扔出去,拿回来一个「未来」的凭证,等需要结果时再问它要。这比直接操作线程舒服多了。

std::async:最简单的异步启动方式

std::async 是 C++11 引入的,用来异步执行一个函数。它会返回一个 std::future 对象,你可以通过这个 future 拿到函数的返回值。

#include <iostream>
#include <future>
#include <chrono>

int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * x;
}

int main() {
    // 异步启动 compute
    std::future<int> result = std::async(std::launch::async, compute, 10);

    std::cout << "正在计算,我先干点别的..." << std::endl;

    // 获取结果,这里会阻塞等待
    int value = result.get();
    std::cout << "计算结果: " << value << std::endl;

    return 0;
}

这里有个细节要注意:std::async 的第一个参数是启动策略。我遇到过不少同事直接不传这个参数,结果在某些编译器上变成了延迟执行(std::launch::deferred),直到调用 get() 才同步执行,异步效果完全没了。

我的建议:明确指定启动策略。用 std::launch::async 强制异步执行,别依赖默认行为。

std::future:异步结果的「凭证」

std::future 就像一个快递单号。你下单(发起异步任务)后拿到单号,可以随时查询包裹状态。但注意,get() 只能调用一次——因为结果被移动走了。第二次调用会抛异常。

我曾经在项目里犯过这个错:多个线程同时调用同一个 future 的 get(),结果程序崩溃了。后来改用 std::shared_future 才解决,它允许多次获取结果。

std::shared_future<int> shared_result = result.share();
// 现在多个线程都可以调用 shared_result.get()

std::promise:手动设置异步结果

std::promise 是 future 的「生产者」。你可以把 promise 传给一个线程,让那个线程在合适的时候设置值。然后通过 promise 关联的 future 来获取这个值。

这玩意儿在哪些场景下有用?我举个例子:你有一个工作线程,它需要从网络接收数据,然后通知主线程处理。用 promise/future 就比用条件变量简洁得多。

void worker(std::promise<std::string> prom) {
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    prom.set_value("数据已就绪");
}

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

    std::thread t(worker, std::move(prom));

    // 主线程等待结果
    std::cout << fut.get() << std::endl;

    t.join();
    return 0;
}
注意:promise 只能设置一次值。重复调用 set_value() 会抛 std::future_error。如果任务失败了,记得调用 set_exception() 传递异常。

std::packaged_task:把函数包装成异步任务

std::packaged_task 是一个可调用对象的包装器。它把函数和 future 绑定在一起。你可以把 packaged_task 扔到线程池里执行,然后通过它关联的 future 获取结果。

我在做高性能计算框架时特别喜欢用这个。它比 std::async 更灵活——你可以控制任务在哪个线程上执行,甚至可以延迟执行。

int add(int a, int b) {
    return a + b;
}

int main() {
    std::packaged_task<int(int, int)> task(add);
    std::future<int> result = task.get_future();

    // 在另一个线程执行
    std::thread t(std::move(task), 3, 4);
    t.detach();

    std::cout << "3 + 4 = " << result.get() << std::endl;
    return 0;
}

核心逻辑关系图

下面这张图展示了 std::async、std::promise、std::packaged_task 和 std::future 之间的关系。你看一眼就明白了。

std::async 自动创建线程执行 std::promise 手动设置值/异常 std::packaged_task 包装可调用对象 std::future / std::shared_future 异步结果的「凭证」 消费者:调用 get() 获取结果 阻塞等待 / 轮询 / 回调 三种方式最终都通过 std::future 获取异步结果

异常处理与注意事项

异步编程中异常处理是个大坑。我曾经在项目里遇到过一个诡异的问题:异步任务抛了异常,但主线程完全没感知,程序静默崩溃了。

后来才发现,std::future::get() 会把异步任务中的异常重新抛出来。所以一定要用 try-catch 包裹 get() 调用。

auto fut = std::async(std::launch::async, []() {
    throw std::runtime_error("任务失败");
});

try {
    fut.get();
} catch (const std::exception& e) {
    std::cerr << "捕获异常: " << e.what() << std::endl;
}
核心要点总结:
  • std::async:快速异步执行,适合简单场景
  • std::promise:手动控制结果传递,适合线程间通信
  • std::packaged_task:灵活的任务包装,适合线程池
  • std::future:统一的结果获取接口,注意只能 get 一次
  • 异常会通过 future 传递,务必捕获处理

嗯,这套机制说白了就是 C++ 给异步编程搭的一个「骨架」。你不需要每次都从零开始造轮子。用好了,代码既清晰又安全。我个人建议你在项目里多试试 std::packaged_task 配合线程池的模式,那才是生产级的玩法。


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