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的使用,触发自定义异常时附加堆栈信息,更容易分析是哪个调用流程出现了异常。

