浏览知识库目录

C++

std::future

std::future

std::future 用于表示一个尚未完成、但未来会产生结果或异常的异步操作。它本身不创建线程,而是结果的“接收端”;结果可由 std::asyncstd::promisestd::packaged_task 提供。

可以把 future 理解成一次性结果通道:生产者设置值或异常,消费者等待并通过 get() 取走结果。

1. future 的基本模型

异步共享状态通常包含:

  • 任务返回值,或任务抛出的异常;
  • 结果是否就绪;
  • 等待结果所需的同步信息;
  • 生产者和消费者之间的生命周期管理。
生产者                                     消费者
std::async / promise / packaged_task  →  std::future
设置值或异常                              wait / get

常用头文件:

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

std::future<T> 表示未来会得到一个 Tstd::future<void> 表示任务只报告完成或异常。

2. 使用 std::async 创建异步任务

最简单的用法:

#include <future>
#include <iostream>

int calculate(int x)
{
    return x * x;
}

int main()
{
    std::future<int> result =
        std::async(std::launch::async, calculate, 12);

    // 主线程可继续做其他工作

    std::cout << result.get() << '\n'; // 144
}

get() 会等待任务完成,然后返回结果。

传递 lambda

auto future = std::async(std::launch::async, [] {
    return std::string("done");
});

std::string value = future.get();

传递成员函数

class Worker {
public:
    int run(int value)
    {
        return value + 1;
    }
};

Worker worker;

auto future = std::async(
    std::launch::async,
    &Worker::run,
    &worker,
    41
);

std::cout << future.get(); // 42

必须保证 worker 在异步任务结束前仍然存活。更安全的做法可能是按值传递对象、使用 shared_ptr,或者明确等待任务完成。

3. async 的启动策略

std::async 有两种主要策略。

std::launch::async

auto f = std::async(std::launch::async, task);

任务异步执行。实现通常创建线程或使用内部执行资源。

std::launch::deferred

auto f = std::async(std::launch::deferred, task);

任务不会立刻执行,而是在第一次调用 get()wait() 的线程中同步执行。

默认策略

auto f = std::async(task);

默认相当于允许实现从 async | deferred 中选择。因此,“调用 async 就一定创建后台线程”是错误的。

如果确实要求并发执行,应显式指定:

std::async(std::launch::async, task);

4. get:获取结果或重新抛出异常

int value = future.get();

get() 有三个重要行为:

  1. 如果结果尚未就绪,则阻塞等待;
  2. 如果任务成功,则返回结果;
  3. 如果任务抛出异常,则在调用 get() 的线程中重新抛出。
auto future = std::async(std::launch::async, []() -> int {
    throw std::runtime_error("calculation failed");
});

try {
    int value = future.get();
} catch (const std::exception& e) {
    std::cerr << e.what();
}

get 只能调用一次

std::future 是一次性消费者。调用 get() 后,future 不再关联共享状态:

int value = future.get();
int again = future.get(); // 抛 std::future_error

如果需要多个消费者读取同一个结果,应使用 std::shared_future

5. valid:检查 future 是否关联共享状态

if (future.valid()) {
    int value = future.get();
}

以下情况通常会使原 future 失效:

  • 调用过 get()
  • future 被移动;
  • 默认构造但尚未接收共享状态。
std::future<int> f1 =
    std::async(std::launch::async, [] { return 42; });

std::future<int> f2 = std::move(f1);

std::cout << f1.valid(); // false
std::cout << f2.valid(); // true

valid() 只表示是否关联共享状态,不表示任务已经完成。

6. wait、wait_for 与 wait_until

wait

future.wait();

阻塞到结果就绪,但不取走结果,也不会在此处重新抛出任务异常。异常仍由后续 get() 抛出。

wait_for

using namespace std::chrono_literals;

auto status = future.wait_for(100ms);

可能返回:

  • std::future_status::ready:结果已就绪;
  • std::future_status::timeout:超时;
  • std::future_status::deferred:任务使用延迟执行策略。
switch (future.wait_for(100ms)) {
case std::future_status::ready:
    std::cout << future.get();
    break;

case std::future_status::timeout:
    std::cout << "still running";
    break;

case std::future_status::deferred:
    std::cout << "will run on wait/get";
    break;
}

wait_until

auto deadline =
    std::chrono::steady_clock::now() +
    std::chrono::seconds(1);

if (future.wait_until(deadline) ==
    std::future_status::ready) {
    std::cout << future.get();
}

超时等待不会取消任务。它只表示调用者不再继续等待。

7. future 析构时为什么可能阻塞

std::async(std::launch::async, ...) 返回的 future,在特定条件下析构可能等待异步任务结束:

std::async(std::launch::async, [] {
    long_running_task();
}); // 临时 future 在语句末尾析构,可能在这里等待

这会让看似异步的代码表现得像同步代码。

正确做法是保存 future,并明确决定何时等待:

auto future = std::async(
    std::launch::async,
    long_running_task
);

// 其他工作
future.get();

future 不是“分离线程”的工具。需要可控线程生命周期时,应考虑 std::jthread、线程池或任务执行器。

8. std::promise:手动提供结果

std::promise<T> 是共享状态的生产端,std::future<T> 是消费端。

#include <future>
#include <thread>

void producer(std::promise<int> promise)
{
    try {
        int result = 42;
        promise.set_value(result);
    } catch (...) {
        promise.set_exception(
            std::current_exception()
        );
    }
}

int main()
{
    std::promise<int> promise;
    std::future<int> future = promise.get_future();

    std::thread worker(
        producer,
        std::move(promise)
    );

    std::cout << future.get();
    worker.join();
}

设置异常

try {
    perform_work();
    promise.set_value(result);
} catch (...) {
    promise.set_exception(std::current_exception());
}

消费者调用 future.get() 时会重新抛出该异常。

broken_promise

如果 promise 在没有设置值或异常的情况下被销毁,future 会就绪,但 get() 会抛出 std::future_error,错误条件为 broken_promise

std::future<int> make_broken_future()
{
    std::promise<int> promise;
    auto future = promise.get_future();
    return future; // promise 被销毁但未设置结果
}

9. promise 的常见限制

  • 每个 promise 只能调用一次 get_future()
  • set_value()set_exception() 只能成功设置一次;
  • promise 不可复制,只能移动;
  • 必须保证所有成功路径和异常路径最终都会设置结果;
  • promise 被移动后,原对象不再拥有共享状态。

set_value_at_thread_exit

promise.set_value_at_thread_exit(value);

值会在线程即将退出时才变为就绪。对应的异常接口是 set_exception_at_thread_exit

10. std::packaged_task:把可调用对象包装成任务

std::packaged_task<R(Args...)> 包装一个可调用对象,并把返回值或异常自动写入关联共享状态。

std::packaged_task<int(int, int)> task(
    [](int a, int b) {
        return a + b;
    }
);

std::future<int> future = task.get_future();

std::thread worker(std::move(task), 20, 22);

std::cout << future.get(); // 42
worker.join();

它常用于线程池:队列中保存任务,提交者持有 future。

template<class F>
auto submit(F&& function)
{
    using Result = std::invoke_result_t<F>;

    auto task = std::make_shared<
        std::packaged_task<Result()>
    >(std::forward<F>(function));

    std::future<Result> future =
        task->get_future();

    enqueue([task] {
        (*task)();
    });

    return future;
}

注意事项

  • packaged_task 只能移动,不能复制;
  • 执行任务后结果才会就绪;
  • 如果任务从未执行且对象被销毁,消费者会收到 broken_promise;
  • 默认构造或被移动后的 task 没有有效共享状态;
  • reset() 可为下一次调用创建新的共享状态,但旧 future 仍关联旧状态。

11. shared_future:让多个消费者共享结果

普通 futureget() 只能调用一次。std::shared_future 可复制,并允许多个线程等待和读取同一共享状态。

std::future<int> future =
    std::async(std::launch::async, [] {
        return 42;
    });

std::shared_future<int> shared =
    future.share();

auto reader = [shared] {
    std::cout << shared.get() << '\n';
};

std::thread t1(reader);
std::thread t2(reader);

t1.join();
t2.join();

也可以直接构造:

std::shared_future<int> shared =
    std::async(std::launch::async, task);

注意事项

  • future.share() 会把共享状态转移给 shared_future,原 future 随后失效;
  • shared_future 对象可安全地在多个线程中分别访问;
  • 如果多个线程操作的是同一个 shared_future 对象本身,仍应避免并发修改;
  • 对于 shared_future<T>get() 返回 const T&,要注意引用生命周期和对象是否可安全共享;
  • shared_future 共享结果,不等于结果对象自身是线程安全的。

12. future

当任务没有返回值时使用 future<void>

std::future<void> future =
    std::async(std::launch::async, [] {
        perform_work();
    });

future.get();

这里 get() 不返回数据,但仍会等待完成并传播异常。因此不要因为任务返回 void 就省略 get。

13. 参数传递与引用陷阱

std::async、线程和 packaged_task 通常会衰减复制参数。

按值传递:

std::string text = "hello";

auto future = std::async(
    std::launch::async,
    [](std::string value) {
        return value.size();
    },
    text
);

显式按引用传递:

int value = 0;

auto future = std::async(
    std::launch::async,
    [](int& x) {
        x = 42;
    },
    std::ref(value)
);

future.get();

使用 std::ref 时必须保证被引用对象在任务完成前仍然存在,并确保并发访问受到正确同步。

14. future 不提供任务取消

标准 std::future 没有 cancel()wait_for 超时也不会停止任务。

可使用协作式取消:

#include <stop_token>
#include <thread>

std::jthread worker([](std::stop_token token) {
    while (!token.stop_requested()) {
        do_one_step();
    }
});

// 稍后
worker.request_stop();

如果必须把结果与取消结合,可让任务同时接收 stop_token,并通过 promise/future 返回结果。

取消是一种协议:任务必须定期检查请求,并在安全点退出。

15. 并行组合的局限

标准 future 没有通用的 thenwhen_allwhen_any 接口。多个 future 常见的简单等待方式:

auto f1 = std::async(
    std::launch::async,
    task1
);

auto f2 = std::async(
    std::launch::async,
    task2
);

auto r1 = f1.get();
auto r2 = f2.get();

大量任务不应无节制地调用 std::async(std::launch::async, ...),否则可能创建过多线程。更适合使用:

  • 有界线程池;
  • 任务调度器;
  • 支持 continuation 的并发库;
  • C++ 协程与合适的异步运行时。

16. 内存可见性与同步保证

当生产者把共享状态设置为 ready,等待该状态的 future 操作会建立必要的同步,使消费者能看到任务在完成前产生的结果。

这意味着:

int data = 0;

auto future = std::async(
    std::launch::async,
    [&] {
        data = 42;
    }
);

future.get();
std::cout << data; // 任务已完成,读取安全

但如果主线程在 get() 前同时读取 data,仍可能发生数据竞争。future 只在正确的等待或结果获取边界上提供同步,不会自动保护所有共享访问。

17. 常见错误

忘记调用 get,导致异常被忽略

auto future = std::async(
    std::launch::async,
    [] {
        throw std::runtime_error("failed");
    }
);

// 没有 future.get(),异常没有在调用线程中被观察

误以为默认 async 一定并发

auto future = std::async(task);

实现可能选择 deferred。要求并发时应显式使用 std::launch::async

对同一个 future 调用两次 get

第一次 get 后 future 已失效。需要多次读取时使用 shared_future。

忽略对象生命周期

异步任务捕获 this、引用或裸指针时,对象可能在任务完成前销毁。

auto future = std::async(
    std::launch::async,
    [this] {
        use_members();
    }
);

应明确等待任务,或使用合适的所有权模型。

把超时当成取消

wait_for 返回 timeout 后,后台任务可能仍在运行并访问资源。

持锁调用 get

std::lock_guard lock(m);
future.get();

如果异步任务也需要 m,就会死锁。一般不要在持锁时等待 future。

18. 如何选择创建方式

需求 推荐方式
简单执行函数并获取结果 std::async
手动控制何时设置值或异常 std::promise
把任务放入队列或线程池 std::packaged_task
多个消费者读取同一结果 std::shared_future
需要可取消线程任务 std::jthread + stop_token
大规模任务调度与组合 线程池、协程或专用异步运行时

19. 实用检查清单

  • 是否明确指定了所需的 async 启动策略?
  • 是否最终调用了 get,从而观察异常?
  • 是否避免对同一 future 调用多次 get?
  • 捕获的引用、裸指针和 this 是否存活足够久?
  • 是否误把 wait_for 超时当成任务取消?
  • 是否可能在持锁时调用 wait 或 get?
  • promise 的所有路径是否都会设置值或异常?
  • packaged_task 是否一定会被执行?
  • 多消费者场景是否应该使用 shared_future?
  • 大量任务是否应该交给有界线程池?
  • 是否明确管理了任务、共享状态和相关对象的生命周期?

20. 总结

  • future 是异步结果的接收端,而不是线程本身;
  • async 适合简单异步调用,但要留意启动策略和析构等待;
  • promise 允许手动交付值或异常;
  • packaged_task 适合把可调用对象接入任务队列;
  • shared_future 适合多个消费者;
  • get() 同时承担等待、取值和异常传播;
  • 超时不等于取消,future 也不会自动解决对象生命周期和数据竞争问题。