1. 项目概述:当异常遇上并发
在C++的世界里,异常和多线程是两个独立且强大的特性,但当它们相遇时,往往会擦出令人头疼的火花。想象一下,你精心设计了一个多线程服务,每个线程都在兢兢业业地处理任务,突然,某个线程深处抛出了一个异常。这个异常会像病毒一样悄无声息地终止它所在的线程,而主线程或其他兄弟线程对此一无所知,程序可能因此陷入一种“部分死亡”的诡异状态,或者更糟,导致资源泄漏和数据不一致。这就是我们今天要啃的硬骨头:如何在并发环境中,让异常能够安全、可控地传播,并被正确地捕获和处理,从而构建出真正健壮的C++多线程应用。
这不仅仅是写个try-catch那么简单。在单线程中,异常沿着调用栈向上“冒泡”,直到被捕获。但在多线程中,每个线程都有自己的调用栈,它们之间是隔离的。一个线程抛出的异常,无法自动穿越线程边界去通知另一个线程。如果你简单地在线程函数内部try-catch然后吞掉异常,那么错误信息就丢失了;如果你不处理,std::thread会直接调用std::terminate结束整个程序,这显然太粗暴了。因此,我们需要一套机制,能够在线程间“搬运”异常,让创建线程的“主控者”有机会知晓并处理子线程中发生的故障。
这个问题的核心价值在于提升程序的可靠性和可维护性。一个健壮的后台服务、一个高性能的计算任务分解框架,或者一个响应的GUI应用(其中耗时操作在后台线程进行),都必须妥善处理工作线程中可能发生的异常,否则一个未处理的异常就可能导致服务不可用、计算任务半途而废,或者界面卡死。通过安全地传播与捕获异常,我们能够实现优雅的错误报告、资源的妥善清理,以及任务的重新调度,这对于构建企业级、生产级的C++并发应用至关重要。
2. 核心挑战与设计思路拆解
2.1 多线程异常处理的根本困境
首先,我们必须认清std::thread与生俱来的“缺陷”。当一个由std::thread启动的线程函数抛出异常且未被自身捕获时,C++标准库会调用std::terminate()。这意味着整个应用程序会立即终止,没有任何商量的余地。这背后的逻辑是,标准库的设计者认为,一个逃离线程函数的异常是一个严重的、无法自动恢复的程序错误,因为没有任何上下文知道该如何处理这个来自另一个执行流的异常。
#include <thread> #include <iostream> void risky_task() { throw std::runtime_error("Oops! Something went wrong in thread."); } int main() { std::thread t(risky_task); t.join(); // 程序在此处会调用 std::terminate() 而崩溃 return 0; }上面的代码就是一个典型的“灾难”场景。线程t内部的异常直接导致了程序终止。这显然不是我们想要的。我们希望主线程能知道子线程失败了,并获取失败的原因,然后决定是重试、记录日志还是优雅关闭。
2.2 核心设计思路:异常的未来化与包装
解决这个问题的通用思路是“异常的未来化”(Exception Futurization)。其核心思想是:将可能发生在子线程中的异常,包装成一个可以被主线程同步等待并获取的结果。这个“结果”里既包含正常返回值,也包含异常信息。
C++标准库为我们提供了完美的工具来实现这一思想:std::promise和std::future。这对组合不仅用于传递值,更关键的是能传递异常。
std::promise:作为异常的生产者(Producer),设置在子线程中。当子线程发生异常时,我们捕获它,并通过promise.set_exception方法将异常指针“存放”进去。std::future:作为异常的消费者(Consumer),由主线程持有。主线程通过调用future.get()来获取结果。如果promise中存放的是异常,那么future.get()会在主线程中重新抛出这个异常,从而实现了异常的“跨线程传播”。
这种设计将异步的错误处理,转变为了同步的异常捕获,使得主线程拥有了对子线程生命周期的完全控制权和错误知情权。
2.3 方案选型:为何是Promise/Future而非其他?
你可能会想到其他方法,比如设置全局错误状态、使用回调函数通知错误,或者使用条件变量同步。我们来简单对比一下:
- 全局错误变量:需要复杂的锁机制来保证线程安全,且主线程需要不断轮询检查,效率低下,耦合度高。
- 错误回调:增加了代码的复杂性,需要管理回调的生命周期,并且在嵌套异步操作时容易陷入“回调地狱”。
- 条件变量:同样需要配合互斥锁和共享状态,实现起来较为繁琐,容易出错。
相比之下,std::promise/std::future方案具有显著优势:
- 类型安全:异常类型信息得以保留。
- 同步简化:
future.get()天然提供了等待机制,无需手动实现轮询或条件变量。 - 标准库支持:是C++11及以后标准的一部分,无需引入第三方库,可移植性好。
- 与
std::async天然集成:std::async返回的future已经内置了异常传播机制,是其更高级别的封装。
因此,基于std::promise和std::future来构建跨线程异常传播机制,是目前C++中最标准、最优雅和最具可维护性的方案。
3. 核心实现:构建安全的异常传播通道
3.1 基础模式:手动包装线程函数
让我们从一个最基础、最清晰的手动模式开始。这个模式明确了每一步的责任,非常适合理解底层原理。
#include <iostream> #include <thread> #include <future> #include <stdexcept> // 子线程实际执行的任务 int compute_something_risky(int input) { if (input < 0) { throw std::invalid_argument("Input cannot be negative!"); } // 模拟一些工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); return input * 2; } // 包装函数,负责捕获异常并设置到promise中 void thread_wrapper(std::promise<int> prom, int input) { try { int result = compute_something_risky(input); prom.set_value(result); // 正常完成,设置值 } catch (...) { // 捕获所有异常,存储异常指针 prom.set_exception(std::current_exception()); } } int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); // 启动线程,传入promise和参数 std::thread t(thread_wrapper, std::move(prom), 10); // 在主线程中等待并获取结果 try { int result = fut.get(); // 可能在此处重新抛出子线程的异常 std::cout << "Result: " << result << std::endl; } catch (const std::exception& e) { std::cerr << "Thread failed with exception: " << e.what() << std::endl; } t.join(); return 0; }关键点解析:
std::promise<int> prom:创建一个用于传递int类型结果的承诺对象。prom.set_exception(std::current_exception()):这是核心。std::current_exception()捕获当前正在处理的异常,并返回一个std::exception_ptr。set_exception将这个异常指针存储到promise中。fut.get():在主线程中调用。如果promise设置了值,则返回值;如果设置了异常,则get()会重新抛出该异常。这个重新抛出发生在调用get()的线程(这里是主线程)中,从而实现了异常的跨线程传播。- 所有权转移:
std::promise通常不能复制,只能移动(std::move)。一个promise只能被设置(值或异常)一次。
注意:
catch (...)是捕获所有异常的必要写法。std::current_exception()即使在捕获...时也能正确工作,它捕获的是异常对象的引用,并构造一个共享所有权的exception_ptr。
3.2 进阶封装:通用的异常安全线程包装器
手动为每个线程函数写包装器太繁琐了。我们可以利用模板和可变参数模板,创建一个通用的包装器,自动处理异常的捕获和传播。
#include <iostream> #include <thread> #include <future> #include <type_traits> #include <utility> // 通用的异常安全线程包装器 template<typename Func, typename... Args> auto make_safe_thread(Func&& func, Args&&... args) -> std::future<typename std::invoke_result_t<Func, Args...>> { using ReturnType = typename std::invoke_result_t<Func, Args...>; std::promise<ReturnType> prom; std::future<ReturnType> fut = prom.get_future(); // 使用lambda捕获所有参数和promise,并移动它们 auto task = [promise = std::move(prom), f = std::forward<Func>(func), ... captured_args = std::forward<Args>(args)]() mutable { try { if constexpr (std::is_void_v<ReturnType>) { // 处理返回void的函数 std::invoke(f, captured_args...); promise.set_value(); } else { // 处理有返回值的函数 auto result = std::invoke(f, captured_args...); promise.set_value(std::move(result)); } } catch (...) { promise.set_exception(std::current_exception()); } }; // 启动线程执行这个lambda任务 std::thread(std::move(task)).detach(); // 注意:这里使用了detach,future是获取结果的唯一句柄 return fut; } // 使用示例 int risky_multiply(int a, int b) { if (a == 0 || b == 0) { throw std::logic_error("Zero factor is not allowed in this context."); } return a * b; } void risky_print(const std::string& msg) { if (msg.empty()) throw std::runtime_error("Message is empty!"); std::cout << "[Thread] " << msg << std::endl; } int main() { // 用例1:带返回值的函数 auto fut1 = make_safe_thread(risky_multiply, 6, 7); auto fut2 = make_safe_thread(risky_multiply, 0, 10); // 这个会抛出异常 // 用例2:返回void的函数 auto fut3 = make_safe_thread(risky_print, "Hello from safe thread!"); auto fut4 = make_safe_thread(risky_print, ""); // 这个会抛出异常 try { std::cout << "Result 1: " << fut1.get() << std::endl; std::cout << "Result 2: " << fut2.get() << std::endl; // 在这里捕获异常 } catch (const std::exception& e) { std::cerr << "Caught exception from fut2: " << e.what() << std::endl; } try { fut3.get(); // 对于void返回类型,get()只是等待完成,不返回值 std::cout << "fut3 completed successfully." << std::endl; fut4.get(); // 在这里捕获异常 } catch (const std::exception& e) { std::cerr << "Caught exception from fut4: " << e.what() << std::endl; } // 主线程需要等待一段时间,因为线程是detach的。 std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; }实现精要:
- 模板推导:
make_safe_thread是一个函数模板,它能自动推导出可调用对象Func的返回类型(通过std::invoke_result_t)和参数类型。 - 完美转发:使用
std::forward保持参数的值类别(左值/右值),避免不必要的拷贝。 - Lambda捕获:Lambda通过初始化捕获(
promise = std::move(prom), ...)将promise和所有参数“移动”到线程内部,所有权清晰,避免了悬空引用。 std::invoke:使用std::invoke来调用函数,它能统一处理普通函数、成员函数、函数对象、lambda等所有可调用类型。if constexpr:在编译期根据返回类型是否为void来生成不同的代码分支,这是C++17的特性,让代码更简洁。- 线程分离:示例中使用了
detach(),这意味着我们放弃了通过std::thread对象对线程的控制,转而完全通过std::future对象来获取结果和异常。这是一种常见的“fire-and-forget”模式,但前提是你确信不需要再join这个线程。
实操心得:在编写此类通用包装器时,要特别注意对象的生命周期。将
promise和所有参数通过移动的方式捕获到lambda中,是保证在线程执行时它们依然有效的关键。如果通过引用捕获局部变量,而局部变量先于线程销毁,就会导致未定义行为。
3.3 使用std::async:标准库的快捷方式
如果你觉得手动创建包装器太麻烦,C++11提供的std::async本身就是一种“异常安全”的线程启动方式。它返回的std::future已经内置了异常传播机制。
#include <iostream> #include <future> #include <vector> #include <numeric> double calculate_average(const std::vector<int>& data) { if (data.empty()) { throw std::invalid_argument("Data vector is empty, cannot calculate average."); } double sum = std::accumulate(data.begin(), data.end(), 0.0); return sum / data.size(); } int main() { std::vector<int> good_data = {1, 2, 3, 4, 5}; std::vector<int> bad_data = {}; // 使用 std::async 启动异步任务 // std::launch::async 策略保证任务会在新线程中执行 auto fut1 = std::async(std::launch::async, calculate_average, good_data); auto fut2 = std::async(std::launch::async, calculate_average, bad_data); // 这个会抛出异常 try { auto avg1 = fut1.get(); std::cout << "Average 1: " << avg1 << std::endl; auto avg2 = fut2.get(); // 异常在此传播 std::cout << "Average 2: " << avg2 << std::endl; } catch (const std::exception& e) { std::cerr << "Caught exception from async task: " << e.what() << std::endl; } return 0; }std::async的优缺点:
- 优点:极其简单,一行代码搞定异步和异常传播。标准库负责线程管理和
promise/future的创建。 - 缺点:控制力较弱。线程的启动策略(
std::launch::async立即在新线程执行,std::launch::deferred延迟到get()时在当前线程执行)需要明确指定,否则由实现定义。对于需要精细控制线程池、优先级或栈大小的复杂场景,std::async可能不够灵活。
注意事项:默认情况下(不指定启动策略),
std::async的实现允许选择是异步执行还是延迟执行。如果你明确要求并发,务必使用std::launch::async策略。否则,在fut.get()时,任务可能在调用线程中同步执行,失去了并发意义。
4. 高级场景与最佳实践
4.1 处理多个并发任务的异常
在实际应用中,我们经常需要启动多个线程并发执行任务,并收集它们的结果和异常。我们可以利用std::future的容器来实现。
#include <iostream> #include <vector> #include <future> #include <random> #include <algorithm> std::vector<std::future<int>> launch_concurrent_tasks(int num_tasks) { std::vector<std::future<int>> futures; std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(-5, 10); // 可能产生负数,触发异常 for (int i = 0; i < num_tasks; ++i) { // 每个任务通过 std::async 启动 futures.emplace_back(std::async(std::launch::async, [i, &dis, &gen]() -> int { int val = dis(gen); std::this_thread::sleep_for(std::chrono::milliseconds(val * 10)); // 模拟耗时 if (val < 0) { throw std::runtime_error("Task " + std::to_string(i) + " generated negative value: " + std::to_string(val)); } return val * val; // 返回平方值 })); } return futures; } int main() { const int num_tasks = 8; auto futures = launch_concurrent_tasks(num_tasks); std::vector<int> successful_results; std::vector<std::string> failure_messages; // 逐一获取结果,处理成功和失败 for (int i = 0; i < num_tasks; ++i) { try { int result = futures[i].get(); successful_results.push_back(result); std::cout << "Task " << i << " succeeded with result: " << result << std::endl; } catch (const std::exception& e) { failure_messages.emplace_back(e.what()); std::cerr << "Task " << i << " failed: " << e.what() << std::endl; } } // 汇总报告 std::cout << "\n--- Summary ---\n"; std::cout << "Successful tasks: " << successful_results.size() << "\n"; std::cout << "Failed tasks: " << failure_messages.size() << "\n"; if (!failure_messages.empty()) { std::cout << "Failure reasons:\n"; for (const auto& msg : failure_messages) { std::cout << " - " << msg << '\n'; } } return 0; }这种模式非常适用于“分而治之”的并行计算场景,如并行处理一批数据文件或同时请求多个网络服务。主线程通过遍历future容器,可以统一处理所有子任务的完成状态(成功或失败),并做出相应的聚合或补偿决策。
4.2 超时与异常:使用std::future_status
在多线程编程中,除了异常,超时也是一个常见问题。我们可以使用future.wait_for()或future.wait_until()来检查任务状态,避免主线程无限期阻塞。
#include <iostream> #include <future> #include <chrono> std::string fetch_data_from_network() { std::this_thread::sleep_for(std::chrono::seconds(7)); // 模拟一个很长的网络请求 return "Network Data"; } int main() { auto fut = std::async(std::launch::async, fetch_data_from_network); auto timeout = std::chrono::seconds(5); // 等待一段时间,检查状态 std::future_status status = fut.wait_for(timeout); switch (status) { case std::future_status::ready: // 任务已完成(可能正常完成,也可能抛出了异常) try { auto data = fut.get(); std::cout << "Data received: " << data << std::endl; } catch (...) { std::cerr << "Task completed but threw an exception.\n"; } break; case std::future_status::timeout: std::cerr << "Task is still running after " << timeout.count() << " seconds. Timeout!\n"; // 在这里可以决定是否取消任务(C++标准线程无法直接取消,但可以设置一个共享的取消标志) break; case std::future_status::deferred: // 如果使用 std::launch::deferred,任务还没开始。通常不会在这里出现。 std::cout << "Task is deferred.\n"; break; } return 0; }重要提示:wait_for返回ready只意味着任务不再运行(已完成或被异常终止)。你仍然需要在get()时用try-catch块来捕获可能存在的异常。超时处理通常需要配合一个“取消标志”(例如一个std::atomic<bool>),通知工作线程尽快结束,因为标准库没有提供直接终止线程的机制。
4.3 异常类型与std::exception_ptr的深入使用
std::exception_ptr是一个类似智能指针的类型,用于共享异常对象。我们可以存储它、传递它,甚至在另一个线程中重新抛出它。
#include <iostream> #include <exception> #include <stdexcept> #include <vector> void handle_exceptions_concurrently(const std::vector<std::exception_ptr>& eptrs) { for (const auto& eptr : eptrs) { if (eptr) { try { std::rethrow_exception(eptr); } catch (const std::runtime_error& e) { std::cerr << "[Runtime Error] " << e.what() << '\n'; } catch (const std::logic_error& e) { std::cerr << "[Logic Error] " << e.what() << '\n'; } catch (...) { std::cerr << "[Unknown Exception]\n"; } } } } int main() { std::vector<std::exception_ptr> exception_bucket; // 模拟在不同上下文中捕获并存储异常 try { throw std::runtime_error("First runtime error"); } catch (...) { exception_bucket.push_back(std::current_exception()); } try { throw std::logic_error("A logic problem"); } catch (...) { exception_bucket.push_back(std::current_exception()); } // 在另一个地方(可以是另一个线程)统一处理这些异常 handle_exceptions_concurrently(exception_bucket); return 0; }这种模式在日志聚合、批量任务错误报告等场景下非常有用。你可以将不同时间、不同线程中捕获的异常先存储起来,最后在一个统一的错误处理中心进行分类、记录或恢复。
5. 常见陷阱、调试技巧与性能考量
5.1 必须避开的“坑”
std::promise的多次设置:一个std::promise对象只能调用一次set_value或set_exception。多次调用会导致抛出std::future_error异常,错误码为std::future_errc::promise_already_satisfied。确保你的执行路径(包括所有try-catch分支)中只设置一次。- 遗忘的
std::future:如果你创建了一个std::promise并获取了其对应的std::future,但最终没有在任何线程调用promise.set_...,并且future被析构了,那么promise在析构时会认为这个“承诺”被打破了(broken promise),默认行为是存储一个std::future_error异常(错误码为std::future_errc::broken_promise)。如果你之后从另一个future(例如通过promise.get_future()再次获取)调用get(),就会得到这个异常。这通常意味着你的逻辑有漏洞,承诺的结果没有被生产出来。 - 线程生命周期与对象析构:这是并发编程的经典问题。确保传递给线程函数的所有参数(尤其是引用和指针)在线程执行期间保持有效。使用值传递或通过智能指针(如
std::shared_ptr)共享所有权是更安全的选择。在上面的通用包装器中,我们使用Lambda的初始化捕获来移动所有资源进去,这是最安全的方式之一。 - 异常与资源释放(RAII):即使在多线程中,RAII(资源获取即初始化)原则仍然是管理资源(内存、文件句柄、锁等)的生命线。确保你的代码中,所有资源都由对象管理(如
std::unique_ptr,std::lock_guard)。这样,即使线程因异常退出,局部对象的析构函数也会被调用,从而保证资源被正确释放。绝对要避免在线程函数中手动new/delete而不使用智能指针。
5.2 调试多线程异常的实用技巧
调试并发程序本就困难,加上异常跨线程传播,问题定位更是雪上加霜。以下是一些实用技巧:
- 输出线程ID:在日志或异常信息中加入
std::this_thread::get_id(),能清晰看到异常是在哪个线程抛出的,又在哪个线程被捕获的。catch (const std::exception& e) { std::cerr << "Thread " << std::this_thread::get_id() << " caught exception: " << e.what() << std::endl; throw; // 重新抛出给promise } - 使用
std::exception_ptr保存上下文:在捕获异常后,除了调用promise.set_exception,你也可以将std::current_exception()存储到某个共享的、线程安全的容器中,供后续深度分析。 - 简化复现:如果可能,尝试在单线程环境下运行可能出错的代码块,或者使用调试器设置条件断点。一些IDE(如Visual Studio、CLion)的调试器对多线程和异常有较好的支持。
- 防御性编程与断言:在关键位置使用
assert或抛出明确的异常(如std::invalid_argument),比让程序因未定义行为而崩溃要好得多。清晰的错误信息是调试的最佳起点。
5.3 性能考量与取舍
异常处理机制本身会带来一些运行时开销(主要是栈展开和类型匹配)。但在多线程异常传播的上下文中,主要的性能考量点不在于异常机制本身,而在于同步和通信开销。
std::future::get()的阻塞:这是一个同步点,调用线程会阻塞直到结果可用。如果任务耗时很长,主线程会被挂起。在设计时,要考虑是否可以使用std::future::wait_for进行超时控制,或者将future放入队列异步处理。std::promise/std::future的内部同步:这对对象内部有同步机制来保证线程安全。在超高并发、微秒级延迟的极端场景下,这可能成为瓶颈。但对于绝大多数应用(如网络服务、数据处理、GUI应用),这个开销是可以接受的。- 与无异常方案的对比:在一些禁用异常的环境(如某些嵌入式系统或高性能计算库),或者为了极致的性能,人们会使用返回错误码(如
std::expected(C++23)或tl::expected(第三方库))或状态标志的方式。这些方案要求调用者显式检查错误,但避免了异常机制的运行时开销。你需要根据项目的性能要求、编码规范和对异常的态度来做出选择。
对于大多数通用C++应用程序,使用基于std::promise/std::future的异常传播是清晰、安全且性能足够的选择。它的最大优势在于将错误处理逻辑自然地集成到了C++的异常体系中,让代码更易于理解和维护。
6. 实战:一个简单的线程池与异常处理集成示例
最后,我们来看一个稍微复杂但更贴近实战的例子:一个简单的固定大小线程池,它能够提交任务并安全地返回结果或异常。
#include <iostream> #include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> #include <future> #include <functional> #include <stdexcept> class SimpleThreadPool { public: explicit SimpleThreadPool(size_t num_threads) : stop(false) { for (size_t i = 0; i < num_threads; ++i) { workers.emplace_back([this] { for (;;) { std::function<void()> task; { std::unique_lock<std::mutex> lock(this->queue_mutex); this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); if (this->stop && this->tasks.empty()) return; task = std::move(this->tasks.front()); this->tasks.pop(); } task(); // 执行任务 } }); } } template<class F, class... Args> auto enqueue(F&& f, Args&&... args) -> std::future<typename std::invoke_result_t<F, Args...>> { using return_type = typename std::invoke_result_t<F, Args...>; auto task = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> res = task->get_future(); { std::unique_lock<std::mutex> lock(queue_mutex); if(stop) throw std::runtime_error("enqueue on stopped ThreadPool"); tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; } ~SimpleThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true; } condition.notify_all(); for(std::thread &worker: workers) worker.join(); } private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; // 使用示例 int main() { SimpleThreadPool pool(4); std::vector<std::future<int>> results; for(int i = 0; i < 8; ++i) { results.emplace_back( pool.enqueue([i] { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << std::endl; if (i == 3) { throw std::runtime_error("Intentional failure in task 3!"); } return i*i; }) ); } // 收集结果,处理异常 for(int i = 0; i < 8; ++i) { try { int value = results[i].get(); std::cout << "Task " << i << " result: " << value << std::endl; } catch (const std::exception& e) { std::cerr << "Task " << i << " threw an exception: " << e.what() << std::endl; } } return 0; }这个线程池的关键点在于enqueue函数:
- 它接受任何可调用对象和其参数。
- 使用
std::packaged_task将可调用对象包装成一个可以返回std::future的任务。std::packaged_task内部已经包含了std::promise的功能。 - 将这个
packaged_task封装到一个无参数的std::function<void()>中,放入任务队列。 - 线程池的工作线程从队列取出并执行这个
std::function,也就是执行了packaged_task。 - 当任务执行时,无论是正常返回还是抛出异常,结果都会被自动存储到
packaged_task内部的promise中。 - 调用者通过
enqueue返回的future.get(),就可以获得结果或捕获到异常。
这个模式将线程管理、任务队列和异常安全完美地结合在了一起,是构建健壮并发应用的坚实基础。你可以在此基础上扩展功能,比如支持优先级队列、动态调整线程数、优雅关闭等。
多线程中的异常处理,本质上是将异步的错误同步化、显式化的过程。std::promise和std::future是C++标准库赐予我们的利器,它们架起了线程间异常传播的桥梁。理解并熟练运用这一机制,结合RAII管理资源,你就能写出既高效又健壮的C++并发程序,从容应对那些在黑暗角落中突然冒出的异常。