在 C++17 之前,异步任务的实现往往依赖第三方线程库或手写线程池。自从 C++11 开始,标准库就提供了 std::async、std::future 和 std::promise,让我们可以轻松地把耗时的工作推迟到后台线程。以下内容将演示如何正确使用这些工具,避免常见陷阱,并给出一些实用的技巧。
1. 基本用法
#include <iostream>
#include <future>
#include <chrono>
int heavyComputation(int x)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * x;
}
int main()
{
// 1. std::async 默认按需调度
std::future <int> f1 = std::async(heavyComputation, 10);
std::cout << "主线程继续执行\n";
std::cout << "结果: " << f1.get() << '\n';
// 2. 明确指定 Launch Policy
std::future <int> f2 = std::async(std::launch::async, heavyComputation, 20);
std::cout << "结果: " << f2.get() << '\n';
}
- 默认调度:若不指定 Launch Policy,编译器可选择
std::launch::async(后台线程)或std::launch::deferred(延迟执行,直到调用get()或wait()时才开始)。 - 显式异步:使用
std::launch::async确保立即在新线程中执行。
2. 异常传播
std::async 的异步函数如果抛出异常,异常会被捕获并存储在 std::future 对象中。只有在调用 get() 时才会重新抛出。
#include <stdexcept>
int riskyOperation()
{
throw std::runtime_error("内部错误");
}
int main()
{
std::future <int> f = std::async(riskyOperation);
try {
f.get(); // 这里会抛出
} catch (const std::exception& e) {
std::cerr << "捕获到异常: " << e.what() << '\n';
}
}
3. 多个等待者:std::shared_future
有时我们希望同一份结果被多个线程共享,而不需要每个线程都单独创建 future。std::shared_future 允许复制,并且只会等待一次。
std::future <int> f = std::async(std::launch::async, heavyComputation, 5);
std::shared_future <int> sf = f.share();
std::thread t1([sf] { std::cout << "t1: " << sf.get() << '\n'; });
std::thread t2([sf] { std::cout << "t2: " << sf.get() << '\n'; });
t1.join(); t2.join();
4. std::packaged_task 与 std::promise
如果你想更细粒度地控制线程与任务之间的关系,可以使用 std::packaged_task:
#include <functional>
std::packaged_task<int(int)> task(heavyComputation);
std::future <int> f = task.get_future();
std::thread worker(std::move(task), 15); // 传递参数
worker.join();
std::cout << "结果: " << f.get() << '\n';
std::promise 则更适合“写者-读者”模式:线程写入值,其他线程读取。
5. 常见陷阱
| 陷阱 | 说明 | 解决办法 |
|---|---|---|
忘记 get() |
future 的析构会调用 wait(),导致主线程卡死 |
明确调用 get() 或 wait(),或者使用 detach() |
| 未检查状态 | future 可能未完成就被拷贝 |
检查 future.valid(),使用 wait_for() / wait_until() |
| 线程泄漏 | std::async 采用 std::launch::async 时,线程会自动 join |
确认你使用 async 的语义,或手动 std::thread join/detach |
| 共享引用导致悬挂 | 任务引用外部对象,生命周期不足 | 使用 std::shared_ptr 或 std::move 确保对象存活 |
6. 小技巧
- 调度策略:在高负载场景下,
std::launch::deferred可以避免创建过多线程。结合future_status::ready与wait_for,可以实现自适应调度。 - 计时:利用
std::chrono::steady_clock与future_status::timeout监测任务超时。 - 错误处理:包装任务时,使用
try-catch并手动设置promise.set_exception,实现更灵活的错误传播。
7. 结语
std::async、std::future 与相关工具为 C++17 提供了一套完整、易用的并发原语。通过了解其调度策略、异常传播机制以及常见陷阱,你可以在不依赖第三方库的情况下,构建高效、可维护的异步代码。下一步可以尝试将这些原语与 std::thread_pool(C++23)或自研线程池结合,进一步提升性能与灵活性。