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的使用,触发自定义异常时附加堆栈信息,更容易分析是哪个调用流程出现了异常。
#秋招##项目##c++后端##投递实习岗位前的准备##牛客创作赏金赛#
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务