异步编程(Future与Promise):std::async、std::future与std::promise、std::packaged_task
聊到异步编程,很多人的第一反应就是「开个线程跑任务」。嗯,这话没错,但不够优雅。你想想看,如果每次异步操作都要手动管理线程、传递结果、处理异常,那代码很快就会变成一团乱麻。
我个人习惯用 std::future 和 std::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;
}
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::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 配合线程池的模式,那才是生产级的玩法。