Linux之日志和线程池、内存池

发布时间:2026/7/27 8:25:43

Linux之日志和线程池、内存池 Linux C 基础组件设计细则日志库 线程池本文档基于提供的代码实现与设计思路从架构思想、类结构、关键实现、避坑要点等维度进行完整拆解覆盖日志库的策略模式设计、线程池的池化与单例设计以及并发编程的核心理论约束。第一部分日志库Log设计细则thread/Log/Log.hpp · bksczm/Linux - 码云 - 开源中国一定边看代码边进看分析后面线程池的代码也是如此1.1 设计目标实现一个可扩展、线程安全、支持流式语法的轻量日志组件支持控制台、文件两种输出方式运行时可动态切换自动携带时间、日志级别、进程号、文件名、行号等定位信息利用 RAII 机制自动刷新输出无需手动调用打印接口多线程环境下保证单条日志的完整性不出现内容交错1.2 核心架构策略模式将「日志输出方式」与「日志格式组装、对外接口」完全解耦是典型的策略模式应用抽象策略基类定义统一的输出接口所有输出方式都遵循该规范具体策略类控制台输出、文件输出分别实现各自的输出逻辑上下文类Log持有策略基类指针负责日志格式组装、策略切换、对外提供调用入口设计收益符合开闭原则新增输出方式如网络日志、滚动文件日志只需新增派生类无需修改 Log 核心代码稀土掘金1.3 类结构与职责划分1.3.1 抽象策略基类Stategyclass 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职责将日志追加写入指定磁盘文件核心执行流程构造函数接收目录路径与文件名使用std::filesystem::exists()判断目录是否存在若不存在调用std::filesystem::create_directories()递归创建多级目录替代系统调用mkdir无需手动处理层级创建调用open()系统调用打开文件标志位为O_APPEND | O_WRONLY | O_CREAT权限设为八进制 0666message()接口中调用write()系统调用写入日志内容析构函数中调用close()关闭文件描述符自动回收资源线程安全继承基类互斥锁写入操作全程加锁1.3.4 上下文类Log职责管理当前输出策略、提供策略切换接口、对外暴露日志调用入口核心成员std::shared_ptrStategy _sta_ptr指向当前输出策略的智能指针负责生命周期管理std::string _path日志文件完整路径核心接口UseConsoleLogStrategy()切换为控制台输出UseFileLogStrategy()切换为文件输出operator()重载函数调用运算符接收日志级别、文件名、行号返回内部类临时对象开启流式日志组装1.3.5 内部类LogmeassageManager设计目的实现LOG(level) msg的流式语法同时利用临时对象的 RAII 特性自动刷新日志核心机制LOG宏调用Log::operator()构造一个LogmeassageManager临时对象构造函数中预组装日志前缀[时间][级别][进程号][文件名][行号] -重载operator每次调用将任意类型内容转为字符串追加到日志消息中语句结束时临时对象析构析构函数自动调用策略的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格式化时必须 1tm_year为 1900 年至今的年数需 19001.4.3 流式输出的正确性约束禁止使用std::endl内部类是将内容写入std::stringendl的 flush 语义无效且会插入多余换行符破坏格式必须用.str()提取完整内容std::stringstream的运算符会按空格分割字符串导致日志内容缺失必须调用.str()获取完整字符串通用类型支持operator内部通过临时stringstream转换任意类型保证所有可流输出类型都能直接使用1.4.4 线程安全保障每个策略实例内置互斥锁输出操作全程加锁保证单条日志的原子性日志前缀组装在临时对象内完成属于线程私有数据无并发竞争文件写入使用O_APPEND模式内核保证追加操作的原子性进一步降低多进程写入风险1.5 核心避坑指南全局对象重定义头文件中仅用extern Log log;声明全局日志对象定义放在独立的.cpp 文件中避免多编译单元包含头文件导致重定义错误函数调用括号不能省getpid()是系统调用函数必须加括号否则会输出函数地址而非进程 ID文件权限注意open的 0666 权限会受系统 umask 掩码影响最终权限为0666 ~umask异常安全std::filesystem::create_directories可能抛出异常必须通过 try-catch 捕获避免程序异常终止路径分隔符使用宏sep统一分隔符便于跨平台适配1.6 现存问题与优化方向命名规范问题Stategy拼写错误应为StrategyLogmeassageManager应为LogMessageManager日志无自动换行当前输出的日志末尾无换行符多条日志会首尾相连应在输出时追加\n缺少级别过滤无法设置最低输出级别生产环境无法关闭 DEBUG 日志可增加全局级别判断文件无滚动机制长期运行会导致单个日志文件过大可新增按大小 / 按天滚动切分功能策略切换不安全切换策略时未加锁多线程并发切换可能导致野指针访问性能优化内部类可持有一个stringstream成员避免每次都构造临时对象第二部分线程池pthread_pool设计细则thread/pthread_pool/pthread_pool.hpp · bksczm/Linux - 码云 - 开源中国2.1 设计目标实现一个线程安全、单例全局唯一、支持优雅退出的固定线程数线程池预创建固定数量工作线程复用线程资源避免频繁创建销毁线程的系统开销支持多生产者并发提交任务工作线程自动消费任务队列停止时保证已入队任务全部执行完成再回收线程资源单例模式保证全局唯一实例避免线程数失控2.2 核心设计思想2.2.1 池化机制池化的本质是资源预分配 复用无池化时每次执行任务都要创建线程陷入内核分配栈空间、创建调度实体任务结束再销毁单次开销大、延迟高线程池预先创建一批线程任务到来时存入队列空闲线程取出执行单个线程生命周期内可执行多个任务收益大幅降低任务启动延迟减少系统调用开销同时控制最大并发数避免系统过载2.2.2 单例模式保证线程池全局唯一采用懒汉模式实现饿汉模式程序启动时就创建实例简单但资源提前占用懒汉模式首次使用时才创建实例实现延时加载延时加载优势资源申请的总开销无法避免但可以推迟到真正使用时再发生对于启动后一段时间才用到线程池的场景避免了前期的资源闲置浪费实现约束构造函数私有化显式删除拷贝构造与赋值运算符 delete禁止外部生成多个实例2.3 类结构与核心成员templatetypename T class pthread_pool { private: std::vectorbksw::pthread _vp; // 工作线程对象数组 std::queueT _qt; // 任务队列 int _thread_num; // 固定线程数量 mutex _mutex; // 保护共享资源的互斥锁 cond _con; // 条件变量实现等待/唤醒 statue _st; // 线程池状态NEW/RUNNING/STOP static pthread_poolT* _sppt; // 单例实例指针 static mutex _lock; // 单例创建保护锁 void Execute(); // 工作线程主循环 public: static pthread_pool* GetInstance(int); templatetypename 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 单例创建双重检查锁定DCLstatic pthread_poolT* GetInstance(int thread_num default_thread_num) { if (_sppt nullptr) { // 外层检查避免每次调用都加锁 guidemutex guard(_lock); // RAII守卫锁自动加解锁 if (_sppt nullptr) { // 内层检查加锁后再次判断保证只创建一次 _sppt new pthread_poolT(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 而不是 ifPOSIX 条件变量存在「虚假唤醒」无 signal/broadcast 也可能从 wait 返回while 循环会重新判断条件保证队列真的有任务才继续执行为什么取完任务立即解锁任务执行时长不可控如果持有锁执行任务其他线程无法取任务、无法提交任务会严重降低并发度退出条件设计必须同时满足「停止状态」和「队列为空」保证所有已提交的任务都执行完毕实现优雅退出2.4.3 任务提交assigntemplatetypename 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::forwardFunc(func), std::forwardArgs(args)...)); _mutex.unlock(); _con.signal(); // 只唤醒一个线程避免惊群 return true; }完美转发std::forward保留参数的值类别减少不必要的拷贝构造std::bind将函数与参数绑定为可调用对象适配任务队列类型emplace入队直接在队列尾部构造对象比push少一次拷贝 / 移动signal单唤醒只唤醒一个等待线程避免「惊群效应」多个线程被唤醒但只有一个能拿到任务其余再次休眠造成无效上下文切换2.4.4 优雅退出机制线程停止时工作线程可能处于三种状态等待锁、执行任务中、等待条件变量退出机制需覆盖所有场景锁内将状态设为STOP保证原子性调用_con.broadcast()唤醒所有等待条件变量的线程各线程退出路径等待条件变量的线程被唤醒后while 条件不成立继续执行判断满足退出条件则解锁退出执行任务中的线程执行完当前任务后回到循环开头判断状态与队列满足条件则退出等待锁的线程拿到锁后判断状态与队列满足条件则退出析构函数自动调用stop()join()保证对象销毁时线程资源正确回收2.5 并发安全设计2.5.1 线程安全保障所有共享资源任务队列、状态变量的读写都在互斥锁保护范围内状态修改与判断都在锁内执行保证原子性单例创建锁与线程池内部锁分离避免锁嵌套风险2.5.2 死锁规避设计死锁的四个必要条件互斥、请求与保持、不剥夺、循环等待。本设计通过以下方式规避死锁锁粒度最小化任务执行在锁外大幅缩短锁持有时间单一锁原则内部仅一把互斥锁不存在多锁嵌套从根源避免循环等待RAII 守卫锁自动解锁避免异常分支忘记释放锁固定状态变更顺序先改状态再广播避免状态不一致导致永久等待扩展多锁场景下可使用std::lock(m1, m2)同时加锁保证原子性也可通过资源一次性分配、超时机制破坏死锁条件银行家算法则是从系统全局角度避免死锁的经典算法2.6 核心避坑指南虚假唤醒必须处理条件变量wait必须放在 while 循环中这是 POSIX 标准的强制要求广播必须在改状态之后如果先广播再改状态线程醒来时状态仍是 RUNNING会再次进入等待导致线程无法退出禁止任务中调用 stop/join工作线程内部调用停止 / 等待接口会导致自己等待自己退出产生死锁线程先构造后启动构造函数只创建线程对象start()才真正启动线程避免构造未完成时线程运行访问未初始化成员移动语义合理使用取任务时用std::move转移资源避免任务对象的深拷贝开销STL 容器非线程安全std::queue、std::vector本身不是线程安全的所有访问必须加锁保护第三部分整体设计原则与共性理论3.1 贯穿的设计原则RAII 资源管理日志临时对象、守卫锁、文件句柄、线程对象全部利用对象生命周期自动管理资源释放从根源避免泄漏与死锁单一职责每个类只负责一项职责策略类只管输出、Log 类只管格式与策略切换、线程池只管线程调度开闭原则日志策略可扩展新增输出方式无需修改核心代码性能优先池化复用资源、锁粒度最小化、移动语义减少拷贝、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也可通过条件编译控制线程池默认线程数、日志默认路径等配置适配不同运行环境。

相关新闻