6、future与async:std::async与std::future、std::promise与std::packaged_task、shared_future、异步任务超时处理
聊到C++并发编程,future和async这组工具,是我个人用得最多的。为什么?因为它们把「异步任务」这件事,包装得特别优雅。你不需要手动管理线程,不需要纠结锁和条件变量,只需要关心「我要什么结果」和「什么时候拿结果」。
说白了,std::async就是帮你开一个后台线程干活,干完了把结果塞进一个std::future里。你这边该干嘛干嘛,等需要结果了,调用future.get()拿数据。嗯,这里要注意——get()是阻塞的,如果任务没跑完,它会一直等。
核心思想:把「任务的发起」和「结果的获取」解耦。你只管发起任务,future帮你兜底。
6.1 std::async 与 std::future:最轻量的异步方案
先看一个最简单的例子。假设你要计算一个耗时的斐波那契数列,但又不想阻塞主流程:
#include <iostream>
#include <future>
#include <chrono>
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
// 启动异步任务
std::future<int> result = std::async(std::launch::async, fibonacci, 40);
// 主线程可以干别的事
std::cout << "主线程继续工作..." << std::endl;
// 需要结果时,阻塞等待
int value = result.get();
std::cout << "计算结果: " << value << std::endl;
return 0;
}
这里std::launch::async是启动策略,告诉系统「必须在新线程里跑」。还有一个策略是std::launch::deferred,它不会立即启动线程,而是等到get()被调用时,才在当前线程里执行。我建议你大部分时候用async策略,除非你有特殊需求。
个人经验:我在项目中遇到过一个问题——用std::async启动大量短任务,结果线程数暴涨,系统负载飙升。后来发现,std::async默认行为是「可能在新线程,也可能在调用get()的线程」,具体看实现。所以如果你要严格控制并发数,建议用std::launch::async显式指定,或者干脆用线程池。
6.2 std::promise 与 std::packaged_task:更灵活的控制
std::async虽然方便,但有时候你需要更精细的控制。比如,你想在某个线程里手动设置一个值,然后让另一个线程通过future拿到它。这时候std::promise就派上用场了。
std::promise和std::future是一对搭档。promise负责「生产」数据,future负责「消费」数据。看个例子:
#include <iostream>
#include <thread>
#include <future>
void worker(std::promise<int> p) {
// 模拟耗时计算
std::this_thread::sleep_for(std::chrono::seconds(2));
p.set_value(42); // 设置结果
}
int main() {
std::promise<int> prom;
std::future<int>> fut = prom.get_future();
std::thread t(worker, std::move(prom));
t.detach();
int result = fut.get(); // 阻塞等待
std::cout << "结果: " << result << std::endl;
return 0;
}
你想想看,这种模式特别适合「生产者-消费者」场景。生产者线程通过promise把结果推出去,消费者线程通过future拉回来。我曾经用这个模式实现过一个日志系统——后台线程负责写日志,主线程通过future知道日志是否写完。
再说std::packaged_task。它本质上是一个「可调用对象的包装器」,把函数调用和future绑定在一起。你可以把packaged_task当作一个任务对象,扔到线程池里执行:
#include <iostream>
#include <future>
#include <thread>
int add(int a, int b) {
return a + b;
}
int main() {
std::packaged_task<int(int, int)> task(add);
std::future<int> fut = task.get_future();
std::thread t(std::move(task), 10, 20);
t.join();
std::cout << "结果: " << fut.get() << std::endl;
return 0;
}
注意:std::promise和std::packaged_task都是只可移动不可复制的。你传递它们给线程时,必须用std::move。我曾经忘了move,编译报错找了半天,嗯,这种坑踩过一次就记住了。
6.3 shared_future:多线程共享结果
普通的std::future只能被一个线程调用get()。如果你有多个线程都需要同一个异步结果,怎么办?用std::shared_future。
shared_future允许多个线程同时等待同一个结果。它内部做了引用计数,每个线程都可以安全地调用get()。看个例子:
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int compute() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 100;
}
int main() {
std::promise<int> prom;
std::shared_future<int> sf = prom.get_future().share();
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([sf]() {
int val = sf.get(); // 每个线程都能拿到结果
std::cout << "线程得到: " << val << std::endl;
});
}
prom.set_value(100); // 触发所有线程
for (auto& t : threads) t.join();
return 0;
}
我个人习惯用shared_future来实现「广播」效果——一个计算结果,通知多个消费者。比如配置加载完成后,通知所有工作线程重新读取配置。
6.4 异步任务超时处理
异步任务最怕什么?怕它卡住不返回。网络请求、数据库查询、外部服务调用,都可能超时。C++标准库没有直接提供future的超时机制,但我们可以自己封装。
思路很简单:用std::future::wait_for()或wait_until()来等待指定时间。如果超时了还没结果,就做降级处理或抛异常。
#include <iostream>
#include <future>
#include <chrono>
int slow_task() {
std::this_thread::sleep_for(std::chrono::seconds(5));
return 42;
}
int main() {
std::future<int> fut = std::async(std::launch::async, slow_task);
// 等待最多2秒
if (fut.wait_for(std::chrono::seconds(2)) == std::future_status::timeout) {
std::cout << "任务超时,做降级处理" << std::endl;
// 这里可以取消任务(但标准库不支持直接取消,需要自己实现)
} else {
int result = fut.get();
std::cout << "结果: " << result << std::endl;
}
return 0;
}
避坑指南:我曾经在线上环境遇到一个问题——某个异步任务超时后,我直接丢弃了future对象。结果那个后台线程还在跑,占着资源不放。后来我意识到,std::future析构时不会自动取消线程。所以超时后,要么你主动通知线程停止(比如用原子变量),要么就接受它继续跑完。没有完美的解决方案,但至少要知道这个坑。
对于更复杂的超时场景,我建议你封装一个TimedFuture类,内部用std::promise和std::thread实现,加上超时检测和取消标志。嗯,这个留给你自己练习了。
知识体系图
这张图把本章的核心知识点串起来了。你看,std::async是最上层的封装,适合快速异步调用;std::promise和std::packaged_task提供了更底层的控制;shared_future解决多线程共享结果的问题;超时处理则是实战中必不可少的兜底机制。
我个人觉得,掌握这五个工具,你就能应对90%的异步编程场景了。剩下的10%,要么是性能调优,要么是特殊业务逻辑,到时候再查文档也来得及。
一个小建议:刚开始用future时,别急着上复杂场景。先写几个简单的async调用,感受一下「异步发起、同步等待」的节奏。等熟悉了,再尝试promise和packaged_task。步子迈大了容易扯着蛋,编程也一样。