C++ 异步任务的异常处理:`std::future` 如何传递异常

好的,没问题!我们直接开始今天的C++异步任务异常处理讲座!

大家好,今天我们来聊聊C++异步任务中一个非常重要,但也经常让人头疼的问题:异常处理。特别是std::future如何传递异常。

想象一下,你开了一家披萨店,雇了一个伙计负责烤披萨(异步任务)。你告诉他:“你去烤个披萨,烤好了告诉我(std::future)。” 结果呢? 伙计可能烤出一个完美的披萨,但也可能把披萨烤糊了(抛出异常)。问题来了,你作为老板,怎么知道披萨烤糊了?又该如何处理这个烂摊子?

这就是异步任务异常处理要解决的问题。std::future就是那个“烤好了告诉我”的机制,而它传递异常的方式,决定了你是否能及时发现问题并采取行动。

一、异步任务,风险与机遇并存

首先,我们要明确一点:异步任务之所以重要,是因为它可以提高程序的并发性和响应速度。你可以同时做很多事情,而不是傻乎乎地等待一个耗时的操作完成。

但是,并发性也带来了风险。如果异步任务执行过程中抛出了异常,如果没有妥善处理,程序可能会崩溃,或者出现难以预料的错误。就像披萨店的伙计把披萨烤糊了,如果没人知道,还卖给顾客,那你的店就完蛋了。

二、std::future:异步结果的守护者

std::future 是 C++ 标准库中用于获取异步操作结果的类。它就像一个承诺,承诺将来会给你一个结果。这个结果可能是值,也可能是异常。

我们可以通过以下方式创建一个 std::future:

  • std::async:启动一个异步任务并返回一个 std::future
  • std::promise:允许手动设置异步任务的结果或异常,然后通过 promise.get_future() 获取对应的 std::future
  • std::packaged_task:将一个函数包装成一个异步任务,并返回一个 std::future

三、异常的传递方式:烤糊的披萨怎么送达?

std::future 传递异常的方式非常巧妙:它不会立即抛出异常,而是将异常“存储”在 std::future 对象中。当调用 future.get()future.wait() 时,如果异步任务抛出了异常,future.get() 会重新抛出该异常。

这就好比伙计把烤糊的披萨用一个盒子装好,然后告诉你:“披萨烤好了!” 当你打开盒子(调用 future.get())时,才会发现里面是烤糊的披萨(异常)。

四、代码示例:烤披萨的正确姿势

下面我们通过一个代码示例来说明 std::future 如何传递异常:

#include <iostream>
#include <future>
#include <stdexcept>

// 模拟烤披萨的任务,可能会烤糊
int bakePizza(int ovenTemperature) {
  std::cout << "开始烤披萨,烤箱温度:" << ovenTemperature << std::endl;
  if (ovenTemperature > 300) {
    std::cout << "温度太高,披萨烤糊了!" << std::endl;
    throw std::runtime_error("披萨烤糊了!温度过高。");
  }
  std::cout << "披萨烤好了!" << std::endl;
  return 10; // 披萨的美味程度,满分10分
}

int main() {
  // 启动异步任务
  std::future<int> pizzaFuture = std::async(std::launch::async, bakePizza, 350);

  try {
    // 获取披萨的美味程度
    int pizzaQuality = pizzaFuture.get();
    std::cout << "披萨的美味程度:" << pizzaQuality << std::endl;
  } catch (const std::exception& e) {
    // 处理异常
    std::cerr << "发生异常:" << e.what() << std::endl;
    std::cerr << "重新启动烤箱,降低温度。" << std::endl;
    // 可以进行一些错误处理,比如重新启动任务
  }

  // 再次启动一个温度正常的异步任务
  std::future<int> goodPizzaFuture = std::async(std::launch::async, bakePizza, 250);

  try {
    int goodPizzaQuality = goodPizzaFuture.get();
    std::cout << "这次的披萨很完美,美味程度:" << goodPizzaQuality << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "再次发生异常:" << e.what() << std::endl;
  }

  return 0;
}

在这个例子中,如果 ovenTemperature 大于 300,bakePizza 函数会抛出一个 std::runtime_error 异常。这个异常会被 std::future 捕获,并存储在 pizzaFuture 对象中。当我们在 main 函数中调用 pizzaFuture.get() 时,这个异常会被重新抛出,然后被 catch 块捕获并处理。

五、future.get() 的特性:只能调用一次

需要注意的是,future.get() 只能调用一次。如果你多次调用 future.get(),并且第一次调用抛出了异常,那么后续的调用也会抛出 std::future_error 异常,错误码为 std::future_errc::broken_promise

这就像你已经打开了装烤糊披萨的盒子,并且处理了问题。你不能再打开同一个盒子,期望里面出现一个完美的披萨。

六、使用 future.wait() 检查异常

如果你只想检查异步任务是否完成,并且不想获取结果(或者你不关心结果),可以使用 future.wait()future.wait_for()

future.wait() 会阻塞当前线程,直到异步任务完成。如果异步任务抛出了异常,future.wait() 不会抛出异常,而是将异常存储在 std::future 对象中,等待 future.get() 调用时再抛出。

future.wait_for() 会阻塞当前线程一段时间,如果异步任务在指定时间内没有完成,future.wait_for() 会返回 std::future_status::timeout。如果异步任务在指定时间内完成了,future.wait_for() 会返回 std::future_status::ready。如果异步任务被延迟,future.wait_for() 会返回 std::future_status::deferred。同样,如果异步任务抛出了异常,future.wait_for() 不会抛出异常。

七、std::promise 的妙用:手动设置结果或异常

std::promise 允许你手动设置异步任务的结果或异常。这在某些情况下非常有用,例如你需要模拟一个异步操作,或者你需要从一个回调函数中设置异步任务的结果。

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

void doSomething(std::promise<int>& promise, bool fail) {
  try {
    if (fail) {
      throw std::runtime_error("操作失败!");
    }
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(2));
    promise.set_value(42); // 设置结果
  } catch (...) {
    promise.set_exception(std::current_exception()); // 设置异常
  }
}

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

  std::thread t(doSomething, std::ref(promise), true); // 模拟失败的情况

  try {
    int result = future.get();
    std::cout << "结果:" << result << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "发生异常:" << e.what() << std::endl;
  }

  t.join();
  return 0;
}

在这个例子中,doSomething 函数接受一个 std::promise 对象作为参数。如果 failtruedoSomething 函数会抛出一个异常,并使用 promise.set_exception() 将异常设置到 promise 对象中。否则,doSomething 函数会设置一个结果值到 promise 对象中。

八、std::packaged_task:将函数包装成异步任务

std::packaged_task 可以将一个函数包装成一个异步任务,并返回一个 std::future。这在需要将一个现有的函数转换为异步任务时非常方便。

#include <iostream>
#include <future>
#include <functional>

int calculateSum(int a, int b) {
  if (a < 0 || b < 0) {
    throw std::invalid_argument("参数不能为负数!");
  }
  return a + b;
}

int main() {
  std::packaged_task<int(int, int)> task(calculateSum);
  std::future<int> future = task.get_future();

  // 启动异步任务
  std::thread t([&task]() {
    try {
      task(10, 20); // 调用函数
    } catch (...) {
      // 捕获函数内部的异常,并传递给 future
      std::cout << "Exception caught in thread" << std::endl;
    }
  });

  try {
    int sum = future.get();
    std::cout << "Sum: " << sum << std::endl;
  } catch (const std::exception& e) {
    std::cerr << "发生异常:" << e.what() << std::endl;
  }

  t.join();
  return 0;
}

在这个例子中,std::packaged_taskcalculateSum 函数包装成一个异步任务。当调用 task(10, 20) 时,calculateSum 函数会被执行。如果 calculateSum 函数抛出一个异常,这个异常会被 std::packaged_task 捕获,并存储在 future 对象中。

九、总结:异常处理的艺术

处理 C++ 异步任务中的异常,就像在披萨店管理员工一样,需要掌握以下技巧:

  • 及时发现问题: 使用 try-catch 块捕获 future.get() 抛出的异常。
  • 不要重复犯错: future.get() 只能调用一次,避免多次调用导致 std::future_error 异常。
  • 未雨绸缪: 使用 future.wait()future.wait_for() 检查异步任务是否完成,但要注意它们不会抛出异常。
  • 灵活应对: 使用 std::promise 手动设置结果或异常,以应对各种复杂场景。
  • 合理包装: 使用 std::packaged_task 将现有函数转换为异步任务。

总的来说,std::future 提供了一种安全可靠的方式来传递异步任务中的异常。只要你理解了它的工作原理,并掌握了上述技巧,就能编写出健壮的并发程序。

十、高级技巧:异常转发

在某些情况下,你可能需要在不同的线程之间转发异常。 C++11 提供了 std::current_exception()std::rethrow_exception() 来实现异常转发。

std::current_exception() 返回一个 std::exception_ptr 对象,它指向当前正在处理的异常。你可以将这个 std::exception_ptr 对象传递给其他线程,然后在其他线程中使用 std::rethrow_exception() 重新抛出该异常。

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

void workerThread(std::promise<void> p) {
    try {
        throw std::runtime_error("Exception from worker thread");
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

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

    std::thread t(workerThread, std::move(p));

    try {
        f.get(); // This will rethrow the exception
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    t.join();
    return 0;
}

表格总结:std::future 异常处理方法

方法 描述 异常处理行为
future.get() 获取异步任务的结果。 如果异步任务抛出异常,future.get() 会重新抛出该异常。只能调用一次,多次调用会抛出 std::future_error
future.wait() 阻塞当前线程,直到异步任务完成。 如果异步任务抛出异常,future.wait() 不会抛出异常,而是将异常存储在 std::future 对象中,等待 future.get() 调用时再抛出。
future.wait_for() 阻塞当前线程一段时间,直到异步任务完成或超时。 如果异步任务抛出异常,future.wait_for() 不会抛出异常,而是将异常存储在 std::future 对象中,等待 future.get() 调用时再抛出。返回 std::future_status 枚举值,指示任务状态(完成、超时、延迟)。
promise.set_value() 设置异步任务的结果。 无异常处理行为。
promise.set_exception() 设置异步任务的异常。 将异常存储在 promise 对象中,当对应的 future.get() 被调用时,会重新抛出该异常。
std::current_exception() 获取当前正在处理的异常的 std::exception_ptr 对象。 用于异常转发,无直接异常处理行为。
std::rethrow_exception() 重新抛出 std::exception_ptr 对象指向的异常。 用于异常转发,用于在其他线程重新抛出异常。
std::packaged_task 将一个函数包装成一个异步任务。 如果函数抛出异常,std::packaged_task 会捕获该异常,并存储在 future 对象中,等待 future.get() 调用时再抛出。

希望这次讲座能帮助大家更好地理解 C++ 异步任务的异常处理。 记住,编写健壮的并发程序,异常处理是至关重要的一环。下次再遇到烤糊的披萨,你也能从容应对了!

祝大家编程愉快!

发表回复

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