4.C++异常处理机制-c++ linux编程:从0实现muduo库系列
重点内容
视频讲解:《C++Linux编程进阶:从0实现muduo C++网络框架系列》-第4讲.C++异常处理机制
主要改动
首先创建第四节课的目录并复制前三节课的内容:
cp -r lesson3 lesson4
muduo网络库框架:
- 实现:base/Exception.h/cc
muduo网络库测试范例
- example/test_exception.cc
CMakeLists.txt 只是添加文件编译的改动,自行用beyondcompare对比
- base/CMakeLists.txt
- examples/CMakeLists.txt
1 异常机制在muduo网络库的应用
1.1 muduo项目中的实际应用
1.1.1 线程运行任务
1.1.2 线程池任务执行应用
1.2 muduo项目中使用异常捕获的作用分析
目多是在执行task()任务时,如果task函数内部调用出现异常但又不处理,如果在在线程或者线程池里不做处理,
那会导致 线程或者线程池崩溃。
通过examples/test_exception.cc 测试范例演示
这个范例测试了三种异常情况。
自定义异常,标准异常可以处理后让线程继续工作,但未定义的异常将导致进程崩溃。
#include "base/Exception.h" #include <stdio.h> #include <vector> #include <execinfo.h> #include <cxxabi.h> #include <string> #include <functional> #include <cstring> // for strchr #include <thread> #include <mutex> #include <condition_variable> #include <queue> #include <sstream> using namespace mymuduo; // 简单的线程池类 class ThreadPool { public: ThreadPool(size_t threads) : stop(false) { for(size_t i = 0; i < threads; ++i) { workers.emplace_back([this] { while(true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(queue_mutex); condition.wait(lock, [this] { return stop || !tasks.empty(); }); if(stop && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); } #if 0 task(); // 这里不捕获异常,会导致线程崩溃 #else // 这里添加异常捕获代码 try { task(); //这里带捕获 } catch (const Exception& e) { printf("1 捕获到自定义异常: %s\n", e.what()); printf("1 异常发生时的堆栈:\n%s\n", e.stackTrace()); } catch (const std::exception& e) { printf("2 捕获到标准异常: %s\n", e.what()); } catch (...) { printf("3 捕获到未知异常\n"); } #endif } }); } } template<class F> void enqueue(F&& f) { { std::unique_lock<std::mutex> lock(queue_mutex); tasks.push(std::forward<F>(f)); } condition.notify_one(); } ~ThreadPool() { { 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; }; // 测试函数:会抛出标准库异常的任务 void throwingSTLTask() { printf("任务开始执行...\n"); std::stringstream ss; // 测试vector访问越界异常 printf("\n=== 测试vector访问越界异常 ===\n"); std::vector<int> vec; vec.push_back(1); vec.push_back(2); printf("尝试访问越界元素...\n"); int value = vec.at(10); // 这里会抛出 std::out_of_range 异常 printf("value:%d\n", value); printf("任务执行完成\n"); } // 测试抛出自定义的异常 void throwingUserDefineTask() { // 测试自定义异常 printf("\n=== 测试自定义异常 ===\n"); throw Exception("这是一个自定义异常"); printf("任务执行完成\n"); } // 测试抛出未定义的异常,比如空指针访问 void throwingNotDefineTask() { // 测试自定义异常 printf("\n=== 测试未定义异常,比如空指针访问 ===\n"); int *p = nullptr; *p = 2; printf("任务执行完成\n"); } int main() { printf("开始测试异常处理...\n"); // 创建一个只有1个线程的线程池 ThreadPool pool(1); printf("提交会抛出异常的任务...\n"); pool.enqueue(throwingSTLTask); pool.enqueue(throwingUserDefineTask); pool.enqueue(throwingNotDefineTask); // 等待一段时间,让任务有机会执行 std::this_thread::sleep_for(std::chrono::seconds(2)); printf("主线程继续执行...\n"); // 提交一个正常的任务 pool.enqueue([]() { printf("这是一个正常的任务\n"); }); // 等待一段时间后退出 std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; }
2 堆栈打印要调用哪些函数
自定义抛出异常时如果要带堆栈信息该如何设置?
具体看base/Exception.cc的void Exception::fillStackTrace(bool demangle) 函数的实现,但不建议深究,这里只讲面试重点:
- backtrace 函数的使用:获取调用栈的地址信息
- backtrace_symbols 函数:将地址转换为可读的符号信息
- abi::__cxa_demangle:C++ 符号的 demangle 处理
- 在编译的时候注意添加 -g 调试信息即可
后续大家也可以参考这个函数的实现 在自己项目实现堆栈信息的打印,主要是用于分析函数调用者,后续项目迭代在讲解reactor网络模型时, 再进一步讲解。
3 Exception自定义异常类实现
这个源码不难,重点是理解 为什么项目中使用了异常机制,以及异常时如何获取堆栈信息。
3.1 异常处理类图
3.2 异常处理流程
3.3 测试用例执行流程
测试用例执行流程说明:
1.初始化阶段
- 创建单线程线程池
- 准备三个测试任务
2.throwingSTLTask 执行流程
- 创建 vector 并添加元素
- 尝试访问越界元素
- 抛出 std::out_of_range 异常
- 被 catch (const std::exception& e) 捕获
3.throwingUserDefineTask 执行流程
- 检查空指针
- 抛出自定义 Exception 异常
- 被 catch (const Exception& e) 捕获
4.throwingNotDefineTask 执行流程
- 直接访问空指针
- 触发段错误
- 被 catch (...) 捕获?
- 段错误发生在操作系统层面 程序直接崩溃,不会进入 C++ 的异常处理机制 因此 catch (...) 块不会被执行。
4 章节总结
- 重点理解:项目中为什么使用异常捕获,主要是为了任务没有及时处理异常时,封装的线程或者线程池能处理对应的异常,目的是使得程序更为健壮。
- 了解: backstrace的使用,触发自定义异常时附加堆栈信息,更容易分析是哪个调用流程出现了异常。