Linux之日志和线程池、内存池
2026/7/27 8:25:28 网站建设 项目流程

Linux C++ 基础组件设计细则(日志库 + 线程池)

本文档基于提供的代码实现与设计思路,从架构思想、类结构、关键实现、避坑要点等维度进行完整拆解,覆盖日志库的策略模式设计、线程池的池化与单例设计,以及并发编程的核心理论约束。


第一部分:日志库(Log)设计细则

thread/Log/Log.hpp · bksczm/Linux - 码云 - 开源中国

一定边看代码边进看分析,后面线程池的代码也是如此

1.1 设计目标

实现一个可扩展、线程安全、支持流式语法的轻量日志组件:

  • 支持控制台、文件两种输出方式,运行时可动态切换
  • 自动携带时间、日志级别、进程号、文件名、行号等定位信息
  • 利用 RAII 机制自动刷新输出,无需手动调用打印接口
  • 多线程环境下保证单条日志的完整性,不出现内容交错

1.2 核心架构:策略模式

将「日志输出方式」与「日志格式组装、对外接口」完全解耦,是典型的策略模式应用:

  • 抽象策略基类:定义统一的输出接口,所有输出方式都遵循该规范
  • 具体策略类:控制台输出、文件输出分别实现各自的输出逻辑
  • 上下文类(Log):持有策略基类指针,负责日志格式组装、策略切换、对外提供调用入口
  • 设计收益:符合开闭原则,新增输出方式(如网络日志、滚动文件日志)只需新增派生类,无需修改 Log 核心代码稀土掘金

1.3 类结构与职责划分

1.3.1 抽象策略基类Stategy
class Stategy { public: virtual void message(const std::string& message) = 0; virtual ~Stategy() = default; mutex _mutex; };
  • 职责:定义所有日志策略的统一输出接口,内置互斥锁保障输出操作的线程安全
  • 强制约束:必须声明虚析构函数,否则通过基类智能指针释放派生类对象时,派生类析构函数不会执行,造成文件句柄泄漏等问题
1.3.2 控制台输出策略ConsoleLogStrategy
  • 职责:将格式化后的日志输出到标准输出流std::cout
  • 实现细节:输出前后加解锁,保证多线程环境下控制台输出不出现行交错
1.3.3 文件输出策略FileLogStrategy
  • 职责:将日志追加写入指定磁盘文件
  • 核心执行流程:
    1. 构造函数接收目录路径与文件名
    2. 使用std::filesystem::exists()判断目录是否存在;若不存在,调用std::filesystem::create_directories()递归创建多级目录(替代系统调用mkdir,无需手动处理层级创建)
    3. 调用open()系统调用打开文件,标志位为O_APPEND | O_WRONLY | O_CREAT,权限设为八进制 0666
    4. message()接口中调用write()系统调用写入日志内容
    5. 析构函数中调用close()关闭文件描述符,自动回收资源
  • 线程安全:继承基类互斥锁,写入操作全程加锁
1.3.4 上下文类Log
  • 职责:管理当前输出策略、提供策略切换接口、对外暴露日志调用入口
  • 核心成员:
    • std::shared_ptr<Stategy> _sta_ptr:指向当前输出策略的智能指针,负责生命周期管理
    • std::string _path:日志文件完整路径
  • 核心接口:
    • UseConsoleLogStrategy():切换为控制台输出
    • UseFileLogStrategy():切换为文件输出
    • operator():重载函数调用运算符,接收日志级别、文件名、行号,返回内部类临时对象,开启流式日志组装
1.3.5 内部类LogmeassageManager
  • 设计目的:实现LOG(level) << "msg"的流式语法,同时利用临时对象的 RAII 特性自动刷新日志
  • 核心机制:
    1. LOG宏调用Log::operator(),构造一个LogmeassageManager临时对象
    2. 构造函数中预组装日志前缀:[时间][级别][进程号][文件名][行号] -
    3. 重载operator<<,每次调用将任意类型内容转为字符串追加到日志消息中
    4. 语句结束时临时对象析构,析构函数自动调用策略的message()接口输出完整日志
  • 设计优势:相比把日志属性直接放在 Log 类中,内部类天然管理单条日志的生命周期,无需手动 flush,也避免多条日志串味

1.4 关键技术实现细节

1.4.1 宏封装与位置信息定位
#define LOG(level) bksw::log(level, __FILE__, __LINE__)
  • __FILE____LINE__是编译器内置宏,分别在预处理阶段替换为当前文件名、当前代码行号
  • 核心约束:这两个宏必须在调用点展开,不能封装到函数内部使用,否则会永远输出函数所在的文件与行号,失去定位能力
1.4.2 时间格式化与格式化符区别

使用线程安全的localtime_r获取本地时间,通过snprintf格式化输出:

snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", ...);

格式化符精确说明:

  • %4d:宽度为 4,右对齐,不足位补空格,用于年份
  • %02d:宽度为 2,右对齐,不足位补 0,用于月、日、时、分、秒,保证固定两位格式
  • %2d:宽度为 2,右对齐,不足位补空格
  • %-2d:宽度为 2,左对齐,不足位补空格
  • %-02d:左对齐时补 0 标志无效,等价于%-2d
  • 注意:tm_mon取值范围为 0-11,格式化时必须 +1;tm_year为 1900 年至今的年数,需 +1900
1.4.3 流式输出的正确性约束
  1. 禁止使用std::endl:内部类是将内容写入std::stringendl的 flush 语义无效,且会插入多余换行符破坏格式
  2. 必须用.str()提取完整内容std::stringstream>>运算符会按空格分割字符串,导致日志内容缺失;必须调用.str()获取完整字符串
  3. 通用类型支持operator<<内部通过临时stringstream转换任意类型,保证所有可流输出类型都能直接使用
1.4.4 线程安全保障
  • 每个策略实例内置互斥锁,输出操作全程加锁,保证单条日志的原子性
  • 日志前缀组装在临时对象内完成,属于线程私有数据,无并发竞争
  • 文件写入使用O_APPEND模式,内核保证追加操作的原子性,进一步降低多进程写入风险

1.5 核心避坑指南

  1. 全局对象重定义:头文件中仅用extern Log log;声明全局日志对象,定义放在独立的.cpp 文件中,避免多编译单元包含头文件导致重定义错误
  2. 函数调用括号不能省getpid()是系统调用函数,必须加括号,否则会输出函数地址而非进程 ID
  3. 文件权限注意open的 0666 权限会受系统 umask 掩码影响,最终权限为0666 & ~umask
  4. 异常安全std::filesystem::create_directories可能抛出异常,必须通过 try-catch 捕获,避免程序异常终止
  5. 路径分隔符:使用宏sep统一分隔符,便于跨平台适配

1.6 现存问题与优化方向

  1. 命名规范问题Stategy拼写错误,应为StrategyLogmeassageManager应为LogMessageManager
  2. 日志无自动换行:当前输出的日志末尾无换行符,多条日志会首尾相连,应在输出时追加\n
  3. 缺少级别过滤:无法设置最低输出级别,生产环境无法关闭 DEBUG 日志,可增加全局级别判断
  4. 文件无滚动机制:长期运行会导致单个日志文件过大,可新增按大小 / 按天滚动切分功能
  5. 策略切换不安全:切换策略时未加锁,多线程并发切换可能导致野指针访问
  6. 性能优化:内部类可持有一个stringstream成员,避免每次<<都构造临时对象

第二部分:线程池(pthread_pool)设计细则

thread/pthread_pool/pthread_pool.hpp · bksczm/Linux - 码云 - 开源中国

2.1 设计目标

实现一个线程安全、单例全局唯一、支持优雅退出的固定线程数线程池:

  • 预创建固定数量工作线程,复用线程资源,避免频繁创建销毁线程的系统开销
  • 支持多生产者并发提交任务,工作线程自动消费任务队列
  • 停止时保证已入队任务全部执行完成,再回收线程资源
  • 单例模式保证全局唯一实例,避免线程数失控

2.2 核心设计思想

2.2.1 池化机制

池化的本质是资源预分配 + 复用

  • 无池化时,每次执行任务都要创建线程(陷入内核分配栈空间、创建调度实体),任务结束再销毁,单次开销大、延迟高
  • 线程池预先创建一批线程,任务到来时存入队列,空闲线程取出执行,单个线程生命周期内可执行多个任务
  • 收益:大幅降低任务启动延迟,减少系统调用开销,同时控制最大并发数,避免系统过载
2.2.2 单例模式

保证线程池全局唯一,采用懒汉模式实现:

  • 饿汉模式:程序启动时就创建实例,简单但资源提前占用
  • 懒汉模式:首次使用时才创建实例,实现延时加载
  • 延时加载优势:资源申请的总开销无法避免,但可以推迟到真正使用时再发生;对于启动后一段时间才用到线程池的场景,避免了前期的资源闲置浪费
  • 实现约束:构造函数私有化,显式删除拷贝构造与赋值运算符(= delete),禁止外部生成多个实例

2.3 类结构与核心成员

template<typename T> class pthread_pool { private: std::vector<bksw::pthread> _vp; // 工作线程对象数组 std::queue<T> _qt; // 任务队列 int _thread_num; // 固定线程数量 mutex _mutex; // 保护共享资源的互斥锁 cond _con; // 条件变量,实现等待/唤醒 statue _st; // 线程池状态:NEW/RUNNING/STOP static pthread_pool<T>* _sppt; // 单例实例指针 static mutex _lock; // 单例创建保护锁 void Execute(); // 工作线程主循环 public: static pthread_pool* GetInstance(int); template<typename Func, typename... Args> bool assign(Func&&, Args&&...); void start(); void stop(); void join(); ~pthread_pool(); };
  • 模板参数T:任务类型,要求是可调用对象(支持operator()
  • 状态机:NEW(新建未启动)→ RUNNING(运行中)→ STOP(停止中),单向流转

2.4 关键流程实现

2.4.1 单例创建:双重检查锁定(DCL)
static pthread_pool<T>* GetInstance(int thread_num = default_thread_num) { if (_sppt == nullptr) { // 外层检查:避免每次调用都加锁 guidemutex guard(_lock); // RAII守卫锁,自动加解锁 if (_sppt == nullptr) { // 内层检查:加锁后再次判断,保证只创建一次 _sppt = new pthread_pool<T>(thread_num); _sppt->start(); } } return _sppt; }
  • 外层判断:99% 以上的调用路径直接返回,无需加锁,保证性能
  • 内层加锁判断:多线程并发首次调用时,保证只有一个线程能创建实例
  • RAII 守卫锁:利用对象生命周期自动解锁,即使中间异常也能释放锁,避免死锁
  • 创建后立即启动线程池,对外透明
2.4.2 工作线程主循环(Execute)

这是线程池最核心的逻辑,严格遵循「加锁 → 判断状态 → 取任务 → 解锁 → 执行任务」的范式:

void Execute() { while (true) { _mutex.lock(); // while循环:规避虚假唤醒,同时持续判断状态 while (_qt.empty() && _st == statue::RUNNING) { _con.wait(_mutex); } // 退出条件:停止状态 且 队列为空 if (_st == statue::STOP && _qt.empty()) { _mutex.unlock(); break; } // 取任务后立即解锁,任务执行不占用锁 T task = std::move(_qt.front()); _qt.pop(); _mutex.unlock(); task(); // 锁外执行任务 } }
  • 为什么用 while 而不是 if:POSIX 条件变量存在「虚假唤醒」(无 signal/broadcast 也可能从 wait 返回),while 循环会重新判断条件,保证队列真的有任务才继续执行
  • 为什么取完任务立即解锁:任务执行时长不可控,如果持有锁执行任务,其他线程无法取任务、无法提交任务,会严重降低并发度
  • 退出条件设计:必须同时满足「停止状态」和「队列为空」,保证所有已提交的任务都执行完毕,实现优雅退出
2.4.3 任务提交(assign)
template<typename Func, typename... Args> bool assign(Func&& func, Args&&... args) { _mutex.lock(); if (_st != statue::RUNNING) { _mutex.unlock(); return false; } // 完美转发 + bind包装,emplace入队减少拷贝 _qt.emplace(std::bind(std::forward<Func>(func), std::forward<Args>(args)...)); _mutex.unlock(); _con.signal(); // 只唤醒一个线程,避免惊群 return true; }
  • 完美转发(std::forward):保留参数的值类别,减少不必要的拷贝构造
  • std::bind:将函数与参数绑定为可调用对象,适配任务队列类型
  • emplace入队:直接在队列尾部构造对象,比push少一次拷贝 / 移动
  • signal单唤醒:只唤醒一个等待线程,避免「惊群效应」(多个线程被唤醒但只有一个能拿到任务,其余再次休眠,造成无效上下文切换)
2.4.4 优雅退出机制

线程停止时,工作线程可能处于三种状态:等待锁、执行任务中、等待条件变量,退出机制需覆盖所有场景:

  1. 锁内将状态设为STOP,保证原子性
  2. 调用_con.broadcast()唤醒所有等待条件变量的线程
  3. 各线程退出路径:
    • 等待条件变量的线程:被唤醒后,while 条件不成立,继续执行,判断满足退出条件则解锁退出
    • 执行任务中的线程:执行完当前任务后回到循环开头,判断状态与队列,满足条件则退出
    • 等待锁的线程:拿到锁后,判断状态与队列,满足条件则退出
  4. 析构函数自动调用stop()+join(),保证对象销毁时线程资源正确回收

2.5 并发安全设计

2.5.1 线程安全保障
  • 所有共享资源(任务队列、状态变量)的读写都在互斥锁保护范围内
  • 状态修改与判断都在锁内执行,保证原子性
  • 单例创建锁与线程池内部锁分离,避免锁嵌套风险
2.5.2 死锁规避设计

死锁的四个必要条件:互斥、请求与保持、不剥夺、循环等待。本设计通过以下方式规避死锁:

  1. 锁粒度最小化:任务执行在锁外,大幅缩短锁持有时间
  2. 单一锁原则:内部仅一把互斥锁,不存在多锁嵌套,从根源避免循环等待
  3. RAII 守卫锁:自动解锁,避免异常分支忘记释放锁
  4. 固定状态变更顺序:先改状态再广播,避免状态不一致导致永久等待

扩展:多锁场景下可使用std::lock(m1, m2)同时加锁,保证原子性;也可通过资源一次性分配、超时机制破坏死锁条件;银行家算法则是从系统全局角度避免死锁的经典算法

2.6 核心避坑指南

  1. 虚假唤醒必须处理:条件变量wait必须放在 while 循环中,这是 POSIX 标准的强制要求
  2. 广播必须在改状态之后:如果先广播再改状态,线程醒来时状态仍是 RUNNING,会再次进入等待,导致线程无法退出
  3. 禁止任务中调用 stop/join:工作线程内部调用停止 / 等待接口,会导致自己等待自己退出,产生死锁
  4. 线程先构造后启动:构造函数只创建线程对象,start()才真正启动线程,避免构造未完成时线程运行访问未初始化成员
  5. 移动语义合理使用:取任务时用std::move转移资源,避免任务对象的深拷贝开销
  6. STL 容器非线程安全std::queuestd::vector本身不是线程安全的,所有访问必须加锁保护

第三部分:整体设计原则与共性理论

3.1 贯穿的设计原则

  1. RAII 资源管理:日志临时对象、守卫锁、文件句柄、线程对象全部利用对象生命周期自动管理资源释放,从根源避免泄漏与死锁
  2. 单一职责:每个类只负责一项职责,策略类只管输出、Log 类只管格式与策略切换、线程池只管线程调度
  3. 开闭原则:日志策略可扩展,新增输出方式无需修改核心代码
  4. 性能优先:池化复用资源、锁粒度最小化、移动语义减少拷贝、DCL 减少加锁次数

3.2 核心并发概念澄清

线程安全 vs 可重入
  • 可重入函数:同一函数在多个执行流中交错调用,仍能正确执行,不依赖全局共享状态
  • 线程安全:多线程并发访问共享资源时,执行结果始终符合预期
  • 关系:
    • 可重入函数一定是线程安全的
    • 线程安全的函数不一定可重入(例如加锁的函数是线程安全的,但在信号处理函数中重入可能导致死锁,因此不可重入)
智能指针的线程安全
  • unique_ptr:仅属于单个线程,所有权唯一,不存在线程安全问题
  • shared_ptr:引用计数的增减是原子操作,引用计数本身线程安全;但指向的对象的读写操作不是线程安全的,仍需加锁保护

3.3 条件编译的工程化应用

可通过条件编译实现日志开关、调试信息控制、默认参数配置:

#ifdef DEBUG #define LOG(level) bksw::log(level, __FILE__, __LINE__) #else // 发布版本编译期消除日志,零运行时开销 #define LOG(level) while(false) bksw::log(level, __FILE__, __LINE__) #endif

也可通过条件编译控制线程池默认线程数、日志默认路径等配置,适配不同运行环境。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询