跳至内容

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()。

async_parallel_sum.cpp
#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()。

promise_demo.cpp
#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 允许多个线程等待并读取同一个结果。

shared_future.cpp
#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` 如何将异常从任务传播到调用者?