如何在 C++17 中使用 std::async 与 std::future 进行异步计算?

在 C++17 之前,异步任务的实现往往依赖第三方线程库或手写线程池。自从 C++11 开始,标准库就提供了 std::asyncstd::futurestd::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

有时我们希望同一份结果被多个线程共享,而不需要每个线程都单独创建 futurestd::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_taskstd::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_ptrstd::move 确保对象存活

6. 小技巧

  • 调度策略:在高负载场景下,std::launch::deferred 可以避免创建过多线程。结合 future_status::readywait_for,可以实现自适应调度。
  • 计时:利用 std::chrono::steady_clockfuture_status::timeout 监测任务超时。
  • 错误处理:包装任务时,使用 try-catch 并手动设置 promise.set_exception,实现更灵活的错误传播。

7. 结语

std::asyncstd::future 与相关工具为 C++17 提供了一套完整、易用的并发原语。通过了解其调度策略、异常传播机制以及常见陷阱,你可以在不依赖第三方库的情况下,构建高效、可维护的异步代码。下一步可以尝试将这些原语与 std::thread_pool(C++23)或自研线程池结合,进一步提升性能与灵活性。

发表评论