std::async、std::future 与 std::promise
使用更高级的并发抽象:通过 std::async 启动异步任务,通过 std::future 获取结果,以及通过 std::promise 手动设置值或异常。
基于任务的并发
虽然 std::thread 提供了底层控制,但使用 std::async 和 std::future 的基于任务的并发通常更简单且更不容易出错。你不再手动管理线程和共享状态,而是将工作表达为产生结果的任务。运行时决定如何调度它们。
std::future 表示一个将在未来可用的类型为 T 的值。你可以调用 get() 来阻塞直到结果就绪。如果任务抛出了异常,get() 会重新抛出它 ── 为你提供跨线程的自动异常传播。
std::async ── 启动异步任务
std::async 启动一个可调用对象并返回持有结果的 std::future。你可以指定启动策略:std::launch::async 强制使用新线程,而 std::launch::deferred 延迟执行直到调用 get()。
#include <iostream>
#include <future>
#include <chrono>
#include <numeric>
#include <vector>
long long parallel_sum(const std::vector<int>& v, size_t start, size_t end) {
if (end - start < 1000) {
return std::accumulate(v.begin() + start, v.begin() + end, 0LL);
}
size_t mid = start + (end - start) / 2;
// Launch left half asynchronously
auto left = std::async(std::launch::async,
parallel_sum, std::cref(v), start, mid);
// Compute right half in this thread
long long right_sum = parallel_sum(v, mid, end);
return left.get() + right_sum; // .get() blocks until left is done
}
int main() {
std::vector<int> data(1'000'000, 1);
auto start = std::chrono::high_resolution_clock::now();
long long result = parallel_sum(data, 0, data.size());
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Sum: " << result << " in " << ms.count() << "ms\n";
return 0;
}启动策略:async 与 deferred
std::launch::async ── 函数在新线程上执行(或从线程池中获取线程,具体取决于实现)。工作立即开始。
std::launch::deferred ── 函数在对期物调用 get() 或 wait() 之前不会执行。它在调用线程中运行(惰性求值)。适用于你可能不需要的计算。
默认(未指定策略) ── 由实现选择。可以是 async、deferred 或两者(意味着在运行时决定)。如果你需要保证并行性,始终指定 std::launch::async。
std::promise ── 手动设置结果
std::promise 是期物-承诺通道的写入端。你创建一个承诺,提取其期物,然后从任何线程调用 set_value() 或 set_exception()。
#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>
void compute(std::promise<int> prom, int x) {
try {
if (x < 0) {
throw std::invalid_argument("negative input");
}
int result = x * x;
prom.set_value(result); // fulfil the promise
} catch (...) {
prom.set_exception(std::current_exception()); // propagate error
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future(); // get the read end
std::thread t(compute, std::move(prom), 7);
try {
int result = fut.get(); // blocks until value or exception
std::cout << "Result: " << result << "\n"; // prints 49
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
t.join();
return 0;
}std::shared_future ── 多个消费者
std::future 只能被 get() 一次(它会移出值)。std::shared_future 允许多个线程等待并读取同一个结果。
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int main() {
auto shared = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}).share(); // convert future to shared_future
// Multiple threads can all read the same result
std::vector<std::thread> consumers;
for (int i = 0; i < 5; ++i) {
consumers.emplace_back([shared, i]() {
int val = shared.get(); // all 5 threads get 42
std::cout << "Consumer " << i << " got: " << val << "\n";
});
}
for (auto& t : consumers) t.join();
return 0;
}由 std::async 返回的 std::future 有一个特殊属性:它的析构函数会阻塞直到任务完成(类似于隐式的 join)。这意味着如果你丢弃了返回的期物,「异步」调用实际上变成了同步调用:
// 意外 ── 这会阻塞!临时期物在分号处被销毁,
// 它会等待任务完成。
std::async(std::launch::async, long_computation);
// 正确 ── 将期物存储起来以让工作并行进行
auto fut = std::async(std::launch::async, long_computation);
// ... 做其他工作 ...
fut.get(); // 现在等待结果这是 std::async 最常见的陷阱。始终将返回的 std::future 保存到变量中。
std::packaged_task
std::packaged_task 包装一个可调用对象并为其结果提供 std::future。与 std::async 不同,它不会自动运行任务 ── 你需要手动调用它(通常是将其移入线程或任务队列)。这使它成为构建自定义线程池的理想选择:
1. 将工作包装在 std::packaged_task 中。
2. 提取 std::future 并交给调用者。
3. 将任务移入工作队列。
4. 工作线程从队列中取出任务并调用它们。
当你想从并发工作中获取结果且不需要细粒度的线程控制时,使用 std::async。它自动传播异常并管理线程生命周期。当你需要持久的后台工作线程、自定义调度,或者正在构建线程池等基础设施时,使用 std::thread(或 std::jthread)。对于「发射后不管」的工作,始终保持返回的 std::future 存活,以避免意外阻塞。
std::async启动任务并返回std::future── 是手动线程管理的高级替代方案std::launch::async强制使用新线程;std::launch::deferred延迟到调用get()时执行std::promise是期物通道的写入端 ── 用于手动设置值/异常std::shared_future让多个线程读取同一个结果std::async返回的std::future在析构时阻塞 ── 始终将其保存到变量中std::packaged_task将可调用对象与期物包装在一起,是自定义线程池的理想选择
测验 — 检验你的知识
(15 经验值)1. 如果你丢弃了 `std::async(std::launch::async, fn)` 返回的 `std::future`,会发生什么?
2. `std::future` 和 `std::shared_future` 之间有什么区别?
3. `std::async` 如何将异常从任务传播到调用者?