C++ `std::promise` 与 `std::future`:异步任务结果传递与等待

好的,各位朋友,欢迎来到今天的C++异步编程小课堂!今天咱们要聊的是C++标准库里一对好基友:std::promisestd::future。 这俩哥们儿,一个负责承诺,一个负责等待,完美诠释了什么叫“信任”。

一、背景故事:为什么需要 promisefuture

想象一下,你是一家餐厅的老板。客人点了菜,厨房开始做菜。客人(主线程)不能傻等着菜做好,得继续招呼其他客人。厨房(异步任务)做好菜后,需要通知客人可以上菜了。

在多线程编程中,我们也经常遇到类似的情况。主线程启动一个异步任务,然后继续做其他事情。异步任务完成计算后,需要把结果传递给主线程。并且,主线程需要在某个时刻等待异步任务的结果。

直接共享变量加锁是个办法,但容易出错,而且代码丑陋。std::promisestd::future 就是为了优雅地解决这个问题而生的。它们提供了一种线程安全的、基于承诺的异步结果传递和等待机制。

二、std::promise:信守承诺的家伙

std::promise 就像厨房里的厨师,负责承诺给客人一道美味佳肴(一个值)。它提供了一种设置异步任务结果的方式。

  • 核心功能: 设置值(set_value)或设置异常(set_exception)。

  • 使用场景: 通常在异步任务中创建,用于存储任务的结果或异常。

  • 代码示例:

#include <iostream>
#include <future>
#include <thread>
#include <exception>

int calculate_sum(int a, int b, std::promise<int> prom) {
  try {
    if (a < 0 || b < 0) {
      throw std::runtime_error("Negative numbers are not allowed!");
    }
    int sum = a + b;
    prom.set_value(sum); // 承诺:我算好啦!
    std::cout << "计算线程: 计算结果 " << sum << " 并设置 promise" << std::endl;
    return sum;
  } catch (const std::exception& e) {
    prom.set_exception(std::current_exception()); // 承诺:我出错了!
    std::cerr << "计算线程: 发生异常: " << e.what() << std::endl;
    return -1; // 或者其他表示错误的返回值
  }
}

int main() {
  std::promise<int> promise; // 创建一个 promise
  std::future<int> future = promise.get_future(); // 获取与 promise 关联的 future

  std::thread t(calculate_sum, 5, 3, std::move(promise)); // 启动异步任务,传递 promise

  try {
    std::cout << "主线程: 等待计算结果..." << std::endl;
    int result = future.get(); // 等待结果,阻塞直到 promise 被设置
    std::cout << "主线程: 收到计算结果: " << result << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "主线程: 捕获到异常: " << e.what() << std::endl;
  }

  t.join(); // 等待线程结束
  return 0;
}

代码解释:

  1. std::promise<int> promise;:创建了一个 promise 对象,它承诺最终会提供一个 int 类型的值。

  2. std::future<int> future = promise.get_future();:从 promise 对象获取一个 future 对象。future 对象是用来获取 promise 所承诺的值的。

  3. std::thread t(calculate_sum, 5, 3, std::move(promise));:创建一个新线程,执行 calculate_sum 函数。注意,promise 对象通过 std::move 传递给线程。这是因为 promise 对象只能与一个 future 对象关联,并且只能被设置一次。

  4. prom.set_value(sum);:在 calculate_sum 函数中,计算完成后,使用 set_value 方法设置 promise 的值。

  5. future.get();:在主线程中,使用 future.get() 方法等待 promise 的值。这个方法会阻塞,直到 promise 的值被设置。如果 promise 设置的是异常,future.get() 会抛出相同的异常。

重点:

  • promise 只能设置一次值或异常。重复设置会导致异常。
  • promise 对象必须在线程之间传递,通常使用 std::move
  • promise 的作用域结束时,如果还没有设置值或异常,会抛出 std::future_error 异常。

三、std::future:耐心等待的家伙

std::future 就像等待上菜的客人,它提供了一种获取异步任务结果的方式。

  • 核心功能: 获取值(get)、检查状态(validwaitwait_forwait_until)。

  • 使用场景: 通常在主线程中创建,用于等待并获取异步任务的结果。

  • 代码示例(延续上面的例子):

(上面的例子已经包含了 future 的基本使用)

future 的更多用法:

  • valid() 检查 future 对象是否有效,即是否与一个 promise 对象关联。
  if (future.valid()) {
    std::cout << "Future 对象有效" << std::endl;
  } else {
    std::cout << "Future 对象无效" << std::endl;
  }
  • wait() 阻塞当前线程,直到 future 对象准备好(即 promise 被设置了值或异常)。
  std::cout << "主线程: 等待 future 准备好..." << std::endl;
  future.wait(); // 阻塞直到 future 准备好
  std::cout << "主线程: future 准备好了!" << std::endl;
  • wait_for() 阻塞当前线程一段时间,如果在指定时间内 future 对象没有准备好,则返回。
  std::cout << "主线程: 等待 future 准备好 (最多 2 秒)..." << std::endl;
  auto status = future.wait_for(std::chrono::seconds(2));

  if (status == std::future_status::ready) {
    std::cout << "主线程: future 准备好了!" << std::endl;
  } else if (status == std::future_status::timeout) {
    std::cout << "主线程: 超时!" << std::endl;
  } else if (status == std::future_status::deferred) {
    std::cout << "主线程: deferred!" << std::endl;
  }
  • wait_until() 阻塞当前线程,直到到达指定的时间点。
  auto time_point = std::chrono::system_clock::now() + std::chrono::seconds(2);
  std::cout << "主线程: 等待 future 准备好 (直到指定时间点)..." << std::endl;
  auto status = future.wait_until(time_point);

  if (status == std::future_status::ready) {
    std::cout << "主线程: future 准备好了!" << std::endl;
  } else if (status == std::future_status::timeout) {
    std::cout << "主线程: 超时!" << std::endl;
  } else if (status == std::future_status::deferred) {
    std::cout << "主线程: deferred!" << std::endl;
  }

重点:

  • future.get() 只能调用一次。再次调用会抛出 std::future_error 异常。
  • 可以使用 wait()wait_for()wait_until() 等方法来控制等待的时间。
  • future 对象可以用于轮询异步任务的状态,但通常更推荐使用 get() 方法阻塞等待结果。

四、std::shared_future:共享的期望

有时候,我们希望多个线程都能访问异步任务的结果。std::future 对象只能被访问一次,之后就失效了。这时,就需要 std::shared_future

  • 核心功能: 允许多个线程共享同一个 future 对象,并获取异步任务的结果。

  • 使用场景: 当多个线程需要同时访问异步任务的结果时。

  • 代码示例:

#include <iostream>
#include <future>
#include <thread>
#include <vector>

int calculate_square(int num, std::promise<int> prom) {
  try {
    int square = num * num;
    prom.set_value(square);
    std::cout << "计算线程: 计算 " << num << " 的平方,结果是 " << square << std::endl;
    return square;
  } catch (const std::exception& e) {
    prom.set_exception(std::current_exception());
    std::cerr << "计算线程: 发生异常: " << e.what() << std::endl;
    return -1;
  }
}

void process_result(std::shared_future<int> shared_future, int thread_id) {
  try {
    int result = shared_future.get();
    std::cout << "线程 " << thread_id << ": 收到结果: " << result << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "线程 " << thread_id << ": 捕获到异常: " << e.what() << std::endl;
  }
}

int main() {
  std::promise<int> promise;
  std::future<int> future = promise.get_future();
  std::shared_future<int> shared_future = future.share(); // 将 future 转换为 shared_future

  std::thread t(calculate_square, 5, std::move(promise));

  std::vector<std::thread> threads;
  for (int i = 0; i < 3; ++i) {
    threads.emplace_back(process_result, shared_future, i + 1); // 创建多个线程,共享 shared_future
  }

  for (auto& thread : threads) {
    thread.join();
  }

  t.join();
  return 0;
}

代码解释:

  1. std::shared_future<int> shared_future = future.share();:将 future 对象转换为 shared_future 对象。

  2. 多个线程 process_result 函数都接收同一个 shared_future 对象。

  3. 每个线程都可以通过 shared_future.get() 方法获取异步任务的结果。

重点:

  • shared_future 可以被多个线程共享。
  • 每个线程都可以独立地调用 shared_future.get() 获取结果。
  • 只有在所有 shared_future 对象都销毁后,底层资源才会被释放。

五、std::packaged_task:任务打包器

std::packaged_task 就像一个任务打包器,它可以将一个函数或可调用对象打包成一个异步任务,并提供访问任务结果的方式。

  • 核心功能: 将函数或可调用对象打包成一个异步任务,并提供一个 future 对象来获取任务结果。

  • 使用场景: 当需要将一个已有的函数或可调用对象放到异步线程中执行时。

  • 代码示例:

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

int multiply(int a, int b) {
  std::cout << "计算线程: 计算 " << a << " * " << b << std::endl;
  return a * b;
}

int main() {
  std::packaged_task<int(int, int)> task(multiply); // 将 multiply 函数打包成一个任务
  std::future<int> future = task.get_future(); // 获取与任务关联的 future

  std::thread t(std::move(task), 6, 7); // 启动线程,执行任务

  std::cout << "主线程: 等待计算结果..." << std::endl;
  int result = future.get(); // 等待结果
  std::cout << "主线程: 收到计算结果: " << result << std::endl;

  t.join();
  return 0;
}

代码解释:

  1. std::packaged_task<int(int, int)> task(multiply);:创建一个 packaged_task 对象,将 multiply 函数打包成一个任务。模板参数 int(int, int) 指定了函数的签名。

  2. std::future<int> future = task.get_future();:获取与任务关联的 future 对象。

  3. std::thread t(std::move(task), 6, 7);:启动线程,执行任务。注意,task 对象通过 std::move 传递给线程。同时,函数的参数也传递给线程。

重点:

  • packaged_task 可以方便地将已有的函数或可调用对象放到异步线程中执行。
  • packaged_task 对象只能被调用一次。
  • packaged_task 可以处理函数抛出的异常,并将异常传递给 future 对象。

六、总结:promisefutureshared_futurepackaged_task 的区别与联系

为了方便大家理解,我们用一个表格来总结一下这几个概念的区别与联系:

特性 std::promise std::future std::shared_future std::packaged_task
作用 设置异步结果 获取异步结果 共享异步结果 打包异步任务
关联对象 future promise future
线程安全
可设置次数 1 N/A N/A N/A
可获取次数 1 1 多次 N/A
所有权 唯一 唯一 共享 唯一
是否可拷贝

联系:

  • promisefuture 通常一起使用,promise 负责设置值,future 负责获取值。
  • shared_futurefuture 的一种特殊形式,允许多个线程共享结果。
  • packaged_task 内部使用 promisefuture 来实现异步任务的封装和结果传递。

七、高级用法:std::async

C++ 标准库还提供了一个方便的函数 std::async,它可以自动地创建一个异步任务,并返回一个 future 对象。

  • 核心功能: 启动一个异步任务,并返回一个 future 对象。

  • 使用场景: 当需要快速地启动一个异步任务时。

  • 代码示例:

#include <iostream>
#include <future>

int calculate_product(int a, int b) {
  std::cout << "计算线程: 计算 " << a << " * " << b << std::endl;
  return a * b;
}

int main() {
  std::future<int> future = std::async(std::launch::async, calculate_product, 8, 9); // 启动异步任务

  std::cout << "主线程: 等待计算结果..." << std::endl;
  int result = future.get(); // 等待结果
  std::cout << "主线程: 收到计算结果: " << result << std::endl;

  return 0;
}

代码解释:

  1. std::future<int> future = std::async(std::launch::async, calculate_product, 8, 9);:使用 std::async 函数启动一个异步任务,执行 calculate_product 函数。

    • std::launch::async:指定异步任务在新线程中执行。
    • calculate_product:要执行的函数。
    • 89:函数的参数。
  2. std::async 函数返回一个 future 对象,可以用于获取异步任务的结果。

std::launch 的两种策略:

  • std::launch::async:强制异步执行,即在新线程中执行。
  • std::launch::deferred:延迟执行,即在调用 future.get()future.wait() 时才执行。

如果不指定 std::launch 策略,std::async 会自动选择合适的策略。

八、最佳实践:避免死锁和资源泄漏

在使用 promisefuture 进行异步编程时,需要注意避免死锁和资源泄漏。

  • 避免死锁: 确保在不同的线程中获取锁的顺序一致。避免循环等待。
  • 避免资源泄漏: 确保 promise 对象最终会被设置值或异常。可以使用 RAII 封装 promise 对象,在对象析构时自动设置一个默认值或异常。

九、总结

std::promisestd::future 是 C++ 异步编程的重要工具。它们提供了一种线程安全的、基于承诺的异步结果传递和等待机制。通过合理地使用它们,可以编写出高效、简洁、可靠的多线程程序。

希望今天的课程对大家有所帮助!记住,异步编程虽然复杂,但只要掌握了核心概念和技巧,就能轻松应对各种挑战。 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注