【Linux系统编程】(四十五)线程池基础:日志系统设计与策略模式的优雅落地


前言

各位 C/C++ 开发者小伙伴们,在实现线程池的过程中,日志系统 是不可或缺的一环 ------ 它能监控线程池的运行状态、记录任务执行的异常信息、帮助我们快速定位线上问题。而如何让日志系统灵活支持控制台输出文件持久化 甚至后续的网络日志等多种输出方式?策略模式就是解决这个问题的最优解之一。

今天这篇文章,我们就从线程池的实际开发需求出发,一步步实现一个基于策略模式的可扩展日志系统,不仅会讲清楚日志系统的核心设计要点,还会深入理解策略模式的设计思想,让你的线程池日志既好用又易扩展!下面就让我们正式开始吧!


一、线程池为何需要专属日志系统?

在开始编码之前,我们先想清楚一个问题:C/C++ 有 printf、cout 这些输出方式,为什么还要专门为线程池设计日志系统?

线程池作为多线程并发的核心组件,其运行过程有高并发、多线程、需追溯的特点,原生输出方式完全满足不了需求,具体体现在这几点:

  1. 缺乏关键信息 :原生输出只有纯文本内容,没有时间戳、日志等级、进程 / 线程 ID、文件名 / 行号等关键信息,出问题后无法快速定位;
  2. 线程安全问题:多线程同时输出日志时,cout/printf 的输出会被打断,导致日志内容错乱,出现 "交叉打印" 的情况;
  3. 输出方式固定:调试时需要日志输出到控制台,线上运行时需要日志持久化到文件,原生输出无法灵活切换;
  4. 无等级区分 :线程池的启动 / 停止 是普通信息、任务入队 / 执行 是调试信息、任务执行失败是错误信息,原生输出无法对日志分级,不利于日志筛选和排查。

简单来说,一个合格的线程池日志系统,必须满足:线程安全、信息完整、输出可配、等级分明 这四个核心要求。而策略模式的引入,能让我们的日志系统在满足这些要求的同时,拥有极佳的可扩展性------ 后续想加新的日志输出方式(比如输出到数据库、网络),无需修改原有代码,直接新增策略即可。

二、前置知识:策略模式到底是什么?

在实现日志系统前,我们先快速搞懂策略模式 ------ 这是一种行为型设计模式,也是开发中最常用的设计模式之一,核心思想非常简单。

2.1 策略模式的核心思想

策略模式的核心是:将算法(行为 / 策略)封装成独立的类,使它们可以相互替换,算法的变化不会影响使用算法的客户端

通俗点说,就是把 "做什么" 和 "怎么做" 分离开:

  • 做什么 :定义一个策略接口,声明算法的统一方法;
  • 怎么做 :为每一种具体的算法实现具体的策略类,继承策略接口;
  • 客户端:持有策略接口的引用,根据需求选择不同的具体策略,无需关心具体实现。

2.2 策略模式的适用场景

当你的业务满足以下特点时,非常适合使用策略模式:

  1. 有多种相似的行为 / 算法,需要根据场景动态切换;
  2. 希望避免大量的if-else/switch判断,让代码更优雅;
  3. 要求算法可扩展,后续新增算法无需修改原有代码。

对应到我们的日志系统:

  • 策略接口 :日志的输出接口(比如SyncLog);
  • 具体策略:控制台日志策略、文件日志策略、网络日志策略(后续可加);
  • 客户端:日志核心类,持有策略接口引用,支持动态切换输出策略。

2.3 策略模式的优点

  1. 高内聚低耦合:每种策略的实现都独立封装,互不干扰;
  2. 易扩展 :新增策略只需实现接口,无需修改原有代码,符合开闭原则
  3. 代码简洁:避免大量条件判断,提高代码可读性和可维护性;
  4. 灵活切换:运行时可动态切换策略,满足不同场景的需求。

搞懂了策略模式,接下来我们就开始一步步实现基于策略模式的线程池日志系统。

三、日志系统的核心设计要点

结合线程池的使用场景,我们先明确日志系统的核心设计规格,做到 "先设计,后编码",避免后续反复修改。

3.1 日志格式规范

一个完整的日志条目,必须包含必备信息,可选信息可根据需求添加,我们设计的日志格式如下(空格分隔,可读性拉满):

复制代码
[时间戳] [日志等级] [进程ID] [文件名] [行号] - 日志内容

示例

复制代码
[2024-08-04 15:09:29] [INFO] [206342] [ThreadPool.hpp] [62] - ThreadPool Construct()
[2024-08-04 15:09:29] [DEBUG] [206342] [ThreadPool.hpp] [109] - 任务入队列成功
[2024-08-04 15:09:39] [ERROR] [206342] [Task.cpp] [36] - 任务执行失败:参数错误
  • 时间戳 :精确到秒,格式YYYY-MM-DD HH:MM:SS,方便日志追溯;
  • 日志等级:DEBUG(调试)、INFO(普通信息)、WARNING(警告)、ERROR(错误)、FATAL(致命错误),分级筛选日志;
  • 进程 ID:多进程环境下定位问题;
  • 文件名 + 行号:快速定位日志打印的代码位置;
  • 日志内容:自定义的业务信息,支持任意类型的拼接(如字符串、数字、字符)。

3.2 核心功能要求

  1. 线程安全:多线程同时打印日志时,保证日志内容不错乱、不丢失;
  2. 策略切换 :支持一键切换控制台日志文件日志,后续可扩展其他策略;
  3. 自动初始化:文件日志策略自动创建日志目录,无需手动创建;
  4. RAII 风格 :利用 C++ 的 RAII 机制,实现日志的自动格式化自动刷新
  5. 宏封装:通过宏定义简化日志调用,自动获取文件名、行号,无需手动传入。

3.3 技术选型

  1. C++11 及以上:使用智能指针、右值引用等特性,提升代码安全性;
  2. POSIX 互斥量 :保证日志输出的线程安全(也可使用 C++11 的std::mutex);
  3. RAII 机制:管理锁和日志对象,避免手动释放资源;
  4. 策略模式:封装不同的日志输出策略,实现灵活切换和扩展;
  5. C++17 文件系统std::filesystem,实现日志目录的自动创建(编译时需加-std=c++17)。

四、一步步实现基于策略模式的日志系统

我们的日志系统采用模块化设计 ,所有代码封装在LogModule命名空间中,分为以下几个部分:

  1. 定义日志等级和基础工具函数(时间戳、等级转字符串);
  2. 实现策略接口(LogStrategy);
  3. 实现具体的策略类(控制台日志ConsoleLogStrategy、文件日志FileLogStrategy);
  4. 实现日志核心类(Logger),持有策略接口,管理策略切换;
  5. 实现 RAII 风格的日志格式化类(LogMessage),支持日志内容拼接;
  6. 宏封装,简化日志调用。

同时,我们会结合之前实现的互斥量封装类MutexLockGuard)保证线程安全,如果你还没有封装互斥量,文末会给出基础的封装代码。

4.1 头文件与命名空间定义

首先创建日志系统的头文件Log.hpp,引入所需的头文件,定义命名空间LogModule,并引入锁的命名空间(保证线程安全)。

cpp 复制代码
// Log.hpp 基于策略模式的线程池日志系统
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <memory>
#include <ctime>
#include <sstream>
#include <filesystem> // C++17 文件系统,需编译参数 -std=c++17
#include <unistd.h>   // getpid() 获取进程ID
#include "Lock.hpp"   // 引入自定义的互斥量封装类

// 日志模块命名空间
namespace LogModule
{
    // 引入锁的命名空间,使用自定义的Mutex和LockGuard
    using namespace LockModule;

    // 日志默认配置:日志目录、日志文件名
    const std::string default_log_path = "./log/";
    const std::string default_log_name = "thread_pool_log.txt";

4.2 定义日志等级与基础工具函数

首先定义枚举类型的日志等级 ,避免魔法数字;然后实现两个基础工具函数:日志等级转字符串获取格式化的当前时间戳,这两个函数是日志格式化的基础。

cpp 复制代码
    // 日志等级:DEBUG(调试) < INFO(信息) < WARNING(警告) < ERROR(错误) < FATAL(致命)
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 工具函数:日志等级转换为字符串,方便日志输出
    std::string LogLevelToString(LogLevel level)
    {
        switch (level)
        {
            case LogLevel::DEBUG:    return "DEBUG";
            case LogLevel::INFO:     return "INFO";
            case LogLevel::WARNING:  return "WARNING";
            case LogLevel::ERROR:    return "ERROR";
            case LogLevel::FATAL:    return "FATAL";
            default:                 return "UNKNOWN";
        }
    }

    // 工具函数:获取格式化的当前时间戳 YYYY-MM-DD HH:MM:SS
    std::string GetCurrentTime()
    {
        // 获取当前时间的时间戳
        time_t now = time(nullptr);
        // 转换为本地时间,使用localtime_r(线程安全),避免localtime(线程不安全)
        struct tm local_tm;
        localtime_r(&now, &local_tm);

        // 格式化时间戳,使用snprintf保证缓冲区安全
        char time_buf[64] = {0};
        snprintf(time_buf, sizeof(time_buf),
                 "%4d-%02d-%02d %02d:%02d:%02d",
                 local_tm.tm_year + 1900,  // 年份从1900开始,需加1900
                 local_tm.tm_mon + 1,      // 月份从0开始,需加1
                 local_tm.tm_mday,
                 local_tm.tm_hour,
                 local_tm.tm_min,
                 local_tm.tm_sec);

        return std::string(time_buf);
    }

关键注意点

  • 使用enum class定义日志等级,是强类型枚举,避免和其他变量重名,比普通枚举更安全;
  • 时间转换使用**localtime_r而非localtime,因为localtime线程不安全**的,多线程环境下会导致时间错乱;
  • 格式化字符串使用snprintf,避免sprintf的缓冲区溢出问题,保证代码安全性。

4.3 实现策略接口:LogStrategy

策略接口是策略模式的核心,我们定义一个纯虚类**LogStrategy,声明唯一的纯虚方法SyncLog,该方法接收格式化后的日志字符串,负责实际的日志输出操作**。

所有具体的日志策略(控制台、文件、网络)都必须继承该接口,并实现SyncLog方法。

cpp 复制代码
    // 策略模式:日志策略接口,声明统一的日志输出方法
    class LogStrategy
    {
    public:
        // 虚析构函数:保证子类析构时能正确调用自身的析构函数
        virtual ~LogStrategy() = default;

        // 纯虚方法:同步输出日志,子类必须实现
        // 参数:格式化后的完整日志字符串
        virtual void SyncLog(const std::string& log_msg) = 0;
    };

关键注意点

  • 必须定义虚析构函数,因为后续日志核心类会持有策略接口的智能指针,析构时需要通过基类指针调用子类的析构函数,避免内存泄漏;
  • 方法命名为**SyncLog(同步日志),因为线程池的日志要求实时刷新**,暂不实现异步日志(异步日志可后续扩展)。

4.4 实现具体策略类:控制台 + 文件日志

接下来实现两个最常用的具体策略类:控制台日志策略ConsoleLogStrategy)和文件日志策略FileLogStrategy),均继承自LogStrategy接口,实现SyncLog方法,并保证线程安全

4.4.1 控制台日志策略:ConsoleLogStrategy

控制台日志策略的核心是将日志输出到标准错误流std::cerr (而非std::cout),因为cerr是无缓冲的,日志会实时输出,而cout是有缓冲的,可能会出现日志延迟。

同时,使用互斥量保证多线程下的输出安全,避免日志交叉。

cpp 复制代码
    // 具体策略1:控制台日志策略 - 日志输出到控制台,方便调试
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        // 实现策略接口的SyncLog方法:控制台输出日志
        void SyncLog(const std::string& log_msg) override
        {
            // RAII风格的锁:自动加锁,析构时自动解锁,保证线程安全
            LockGuard lock(_mutex);
            // 输出到标准错误流,实时刷新,无缓冲
            std::cerr << log_msg << std::endl;
        }

    private:
        Mutex _mutex; // 互斥量,保证多线程控制台输出的线程安全
    };

4.4.2 文件日志策略:FileLogStrategy

文件日志策略的核心是将日志追加写入到指定的日志文件中,核心功能包括:

  1. 构造函数中自动创建日志目录(如果不存在);
  2. SyncLog方法中以追加模式打开日志文件,写入日志;
  3. 使用互斥量保证多线程下的文件写入安全,避免文件内容错乱。
cpp 复制代码
    // 具体策略2:文件日志策略 - 日志持久化到文件,线上环境使用
    class FileLogStrategy : public LogStrategy
    {
    public:
        // 构造函数:初始化日志路径和文件名,自动创建日志目录
        FileLogStrategy(const std::string& log_path = default_log_path,
                        const std::string& log_name = default_log_name)
            : _log_path(log_path), _log_name(log_name)
        {
            // 加锁保证目录创建的线程安全
            LockGuard lock(_mutex);
            // 判断日志目录是否存在,不存在则创建(递归创建,支持多级目录)
            if (!std::filesystem::exists(_log_path))
            {
                try
                {
                    std::filesystem::create_directories(_log_path);
                }
                catch (const std::filesystem::filesystem_error& e)
                {
                    // 目录创建失败,输出错误信息到控制台
                    std::cerr << "创建日志目录失败:" << e.what() << std::endl;
                }
            }
        }

        // 实现策略接口的SyncLog方法:将日志追加写入文件
        void SyncLog(const std::string& log_msg) override
        {
            // RAII风格的锁:保证多线程文件写入的线程安全
            LockGuard lock(_mutex);
            // 拼接完整的日志文件路径
            std::string log_file = _log_path + _log_name;
            // 以追加模式打开文件:std::ios::app,不存在则创建,存在则追加
            std::ofstream log_fs(log_file, std::ios::app);
            if (!log_fs.is_open())
            {
                std::cerr << "打开日志文件失败:" << log_file << std::endl;
                return;
            }
            // 写入日志并换行
            log_fs << log_msg << std::endl;
            // 手动刷新缓冲区,保证日志实时写入文件
            log_fs.flush();
        }

    private:
        std::string _log_path;  // 日志目录
        std::string _log_name;  // 日志文件名
        Mutex _mutex;           // 互斥量,保证多线程文件操作的线程安全
    };

关键注意点

  • 使用std::filesystem::create_directories而非create_directory,前者支持递归创建多级目录 (比如./log/2024/08/),后者只能创建单级目录;
  • 文件打开模式使用std::ios::app,保证日志追加写入,不会覆盖原有内容;
  • 写入后调用flush()手动刷新缓冲区,保证日志实时写入文件,避免程序崩溃时日志丢失;
  • 对文件系统操作做异常捕获,因为目录创建、文件打开可能会因权限问题失败,避免程序崩溃。

4.5 实现日志核心类:Logger

日志核心类Logger是策略模式的客户端 ,它持有策略接口LogStrategy智能指针std::unique_ptr),负责:

  1. 管理日志策略的初始化切换(控制台 / 文件);
  2. 创建日志格式化对象LogMessage),封装日志的格式化逻辑;
  3. 作为日志系统的入口,对外提供简洁的调用接口。

同时,我们将Logger的构造函数默认初始化为控制台日志策略,方便调试。

cpp 复制代码
    // 日志核心类:策略模式的客户端,持有策略接口,管理策略切换,创建日志对象
    class Logger
    {
    public:
        // 构造函数:默认使用控制台日志策略,方便开发调试
        Logger()
        {
            UseConsoleStrategy();
        }

        // 析构函数:默认即可,智能指针自动释放策略对象
        ~Logger() = default;

        // 切换策略:使用控制台日志
        void UseConsoleStrategy()
        {
            _log_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 切换策略:使用文件日志,支持自定义日志路径和文件名
        void UseFileStrategy(const std::string& log_path = default_log_path,
                             const std::string& log_name = default_log_name)
        {
            _log_strategy = std::make_unique<FileLogStrategy>(log_path, log_name);
        }

        // 内部类:RAII风格的日志格式化类,下文实现
        class LogMessage;

        // 创建日志格式化对象,作为日志输出的入口
        // 参数:日志等级、文件名、行号(后续通过宏自动传入)
        LogMessage operator()(LogLevel level, const std::string& file_name, int line_num);

    private:
        // 持有日志策略接口的智能指针,支持动态切换策略
        // std::unique_ptr:独占所有权,避免策略对象被多次拷贝
        std::unique_ptr<LogStrategy> _log_strategy;
    };

关键注意点

  • 使用**std::unique_ptr持有策略对象,而非原始指针,利用智能指针的RAII 机制**自动释放资源,避免内存泄漏;
  • 策略切换方法**UseConsoleStrategy/UseFileStrategy通过std::make_unique创建具体的策略对象,赋值给策略接口指针,实现策略的动态替换**;
  • 核心类不直接实现日志格式化,而是通过内部类LogMessage实现,让职责更单一,符合单一职责原则

4.6 实现 RAII 风格的日志格式化类:LogMessage

LogMessage是**Logger的内部类,采用RAII 风格** 设计,是日志系统的格式化核心,核心职责:

  1. 构造时 :自动格式化日志的头部信息(时间戳、等级、进程 ID、文件名、行号);
  2. 重载 << 运算符:支持任意类型的日志内容拼接(字符串、数字、字符等);
  3. 析构时 :自动调用策略的SyncLog方法,输出格式化后的完整日志。

这种设计的好处是:日志对象创建时格式化头部,内容拼接完成后,对象析构时自动输出日志,无需手动调用刷新方法,使用非常简洁。

cpp 复制代码
    // 内部类:RAII风格的日志格式化类 - 构造格式化头部,析构自动输出日志
    class Logger::LogMessage
    {
    private:
        LogLevel _level;          // 日志等级
        std::string _time;        // 格式化后的时间戳
        pid_t _pid;               // 进程ID
        std::string _file_name;   // 日志打印的文件名
        int _line_num;            // 日志打印的行号
        Logger& _logger;          // 引用外部的Logger对象,用于调用策略的SyncLog
        std::string _log_msg;     // 格式化后的完整日志字符串

    public:
        // 构造函数:自动格式化日志头部信息
        LogMessage(LogLevel level, const std::string& file_name, int line_num, Logger& logger)
            : _level(level), _file_name(file_name), _line_num(line_num), _logger(logger)
        {
            // 初始化基础信息
            _time = GetCurrentTime();
            _pid = getpid(); // 获取当前进程ID

            // 格式化日志头部:[时间] [等级] [PID] [文件] [行号] - 
            std::stringstream ss;
            ss << "[" << _time << "] "
               << "[" << LogLevelToString(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _file_name << "] "
               << "[" << _line_num << "] - ";
            // 将头部信息存入完整日志字符串
            _log_msg = ss.str();
        }

        // 重载<<运算符:支持任意类型的日志内容拼接,返回自身引用支持链式调用
        template <typename T>
        LogMessage& operator<<(const T& content)
        {
            std::stringstream ss;
            ss << content;
            _log_msg += ss.str();
            return *this;
        }

        // 析构函数:RAII核心 - 自动调用策略的SyncLog方法,输出完整日志
        ~LogMessage()
        {
            if (_logger._log_strategy) // 策略对象不为空时才输出
            {
                _logger._log_strategy->SyncLog(_log_msg);
            }
        }
    };

    // 实现Logger的operator()方法:创建LogMessage对象
    inline Logger::LogMessage Logger::operator()(LogLevel level, const std::string& file_name, int line_num)
    {
        // 返回LogMessage临时对象,RAII风格
        return LogMessage(level, file_name, line_num, *this);
    }

关键注意点

  1. 模板重载 << 运算符 :使用模板可以支持任意类型的日志内容拼接(int、double、std::string、char 等),无需为每种类型单独重载,代码更简洁;
  2. 引用外部 Logger_loggerLogger非 const 引用,保证能调用到 Logger 的策略对象;
  3. 析构时自动输出 :这是 RAII 的核心,当**LogMessage临时对象的生命周期结束时(比如一行日志拼接完成),析构函数自动调用SyncLog**输出日志,无需手动操作;
  4. 内联 operator () 方法 :将方法定义为**inline**,避免链接错误(内部类的外部方法实现需要内联)。

4.7 宏封装:简化日志调用,自动获取文件名和行号

现在我们的日志系统已经实现了核心功能,但调用时需要手动传入文件名行号 ,非常繁琐。我们可以通过C/C++ 的预定义宏 实现自动获取文件名和行号 ,并封装成简洁的日志宏,让日志调用像cout一样简单。

同时,封装策略切换的宏,一键切换控制台 / 文件日志。

cpp 复制代码
    // 定义全局的Logger对象,整个线程池共用一个日志实例
    Logger g_logger;

    // 日志宏:自动获取文件名(__FILE__)和行号(__LINE__),简化调用
    // 用法:LOG(LogLevel::INFO) << "线程池启动成功,线程数:" << 10;
    #define LOG(level) g_logger(level, __FILE__, __LINE__)

    // 策略切换宏:一键启用控制台日志/文件日志
    #define ENABLE_CONSOLE_LOG() g_logger.UseConsoleStrategy()
    #define ENABLE_FILE_LOG(path, name) g_logger.UseFileStrategy(path, name)
    // 重载:使用默认路径和文件名的文件日志
    #define ENABLE_FILE_LOG_DEFAULT() g_logger.UseFileStrategy()

} // end of namespace LogModule

C/C++ 预定义宏说明

  • __FILE__:当前源文件的完整路径和文件名(可通过编译器参数简化为文件名);
  • __LINE__:当前代码的行号,整数类型;
  • 这两个宏在预编译阶段会被编译器替换为实际的字符串和数字,无需手动传入。

日志调用示例

cpp 复制代码
LOG(LogLevel::INFO) << "线程池初始化完成,核心线程数:" << 10;
LOG(LogLevel::DEBUG) << "任务入队成功,任务ID:" << 1001;
LOG(LogLevel::ERROR) << "任务执行失败,原因:" << "参数为空";

是不是非常简洁?和cout的使用方式几乎一致,还自带所有日志头部信息!

4.8 配套的互斥量封装类:Lock.hpp

如果你的项目中还没有封装互斥量,这里给出基础的**MutexLockGuard**封装代码(Lock.hpp),保证日志系统的线程安全,也是后续线程池开发的基础。

cpp 复制代码
// Lock.hpp 互斥量封装类,RAII风格,保证线程安全
#pragma once
#include <iostream>
#include <pthread.h>

namespace LockModule
{
    // 互斥量封装类
    class Mutex
    {
    public:
        // 禁用拷贝和赋值:互斥量不能被拷贝
        Mutex(const Mutex&) = delete;
        Mutex& operator=(const Mutex&) = delete;

        // 构造函数:初始化互斥量
        Mutex()
        {
            if (pthread_mutex_init(&_mutex, nullptr) != 0)
            {
                std::cerr << "互斥量初始化失败!" << std::endl;
                exit(EXIT_FAILURE);
            }
        }

        // 加锁
        void Lock()
        {
            if (pthread_mutex_lock(&_mutex) != 0)
            {
                std::cerr << "互斥量加锁失败!" << std::endl;
                exit(EXIT_FAILURE);
            }
        }

        // 解锁
        void Unlock()
        {
            if (pthread_mutex_unlock(&_mutex) != 0)
            {
                std::cerr << "互斥量解锁失败!" << std::endl;
                exit(EXIT_FAILURE);
            }
        }

        // 析构函数:销毁互斥量
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex; // 原生POSIX互斥量
    };

    // RAII风格的锁守卫:构造加锁,析构解锁,避免忘记解锁
    class LockGuard
    {
    public:
        // 禁用拷贝和赋值
        LockGuard(const LockGuard&) = delete;
        LockGuard& operator=(const LockGuard&) = delete;

        // 构造函数:传入互斥量引用,加锁
        explicit LockGuard(Mutex& mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        }

        // 析构函数:解锁
        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        Mutex& _mutex; // 互斥量引用,避免拷贝
    };
} // end of namespace LockModule

五、日志系统的使用示例

日志系统实现完成后,使用非常简单,我们写一个测试程序log_test.cpp,演示日志的调用、策略的切换,以及多线程下的线程安全测试。

5.1 测试代码:log_test.cpp

cpp 复制代码
// log_test.cpp 日志系统测试程序
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "Log.hpp"
#include "Lock.hpp"

using namespace LogModule;
using namespace LockModule;

// 多线程测试函数:多个线程同时打印日志,测试线程安全
void* log_thread_func(void* arg)
{
    std::string thread_name = (char*)arg;
    for (int i = 0; i < 5; ++i)
    {
        LOG(LogLevel::DEBUG) << thread_name << " 打印调试日志,次数:" << i;
        LOG(LogLevel::INFO) << thread_name << " 打印信息日志,次数:" << i;
        usleep(100000); // 休眠100ms,模拟业务逻辑
    }
    return nullptr;
}

int main()
{
    // 1. 默认使用控制台日志,打印测试日志
    std::cout << "===== 控制台日志测试 =====" << std::endl;
    LOG(LogLevel::INFO) << "日志系统启动成功,默认使用控制台日志";
    LOG(LogLevel::WARNING) << "这是一条警告日志,测试等级输出";
    LOG(LogLevel::ERROR) << "这是一条错误日志,测试内容拼接:" << 123 << " " << 3.14 << " " << 'c';
    LOG(LogLevel::FATAL) << "这是一条致命错误日志,测试进程ID:" << getpid();

    // 2. 切换为文件日志(默认路径./log/,文件名thread_pool_log.txt)
    std::cout << "\n===== 切换为文件日志 =====" << std::endl;
    ENABLE_FILE_LOG_DEFAULT();
    LOG(LogLevel::INFO) << "成功切换为文件日志,日志文件路径:./log/thread_pool_log.txt";

    // 3. 多线程日志测试,创建3个线程同时打印日志,测试线程安全
    std::cout << "\n===== 多线程日志测试 =====" << std::endl;
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, log_thread_func, (void*)"Thread-1");
    pthread_create(&t2, nullptr, log_thread_func, (void*)"Thread-2");
    pthread_create(&t3, nullptr, log_thread_func, (void*)"Thread-3");

    // 等待线程结束
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    // 4. 切换回控制台日志
    std::cout << "\n===== 切换回控制台日志 =====" << std::endl;
    ENABLE_CONSOLE_LOG();
    LOG(LogLevel::INFO) << "日志系统测试完成,所有日志打印正常";

    return 0;
}

5.2 编译与运行

由于我们使用了C++17 的文件系统 ,编译时需要添加**-std=c++17-lpthread**(POSIX 线程库)参数:

bash 复制代码
# 编译命令
g++ log_test.cpp Lock.hpp Log.hpp -o log_test -std=c++17 -lpthread

# 运行程序
./log_test

5.3 运行结果

  1. 控制台会输出所有日志,格式符合我们的设计,多线程打印的日志无错乱;
  2. 程序运行后会自动创建./log/目录,并生成thread_pool_log.txt文件,文件中会保存切换为文件日志后的所有日志;
  3. 日志中包含完整的时间戳、等级、进程 ID、文件名、行号,信息完整。

六、将日志系统集成到线程池

将日志系统集成到线程池非常简单,只需在 ThreadPool 的头文件中引入Log.hpp,并在关键节点打印日志即可,比如:

  1. 线程池构造时:打印INFO日志,提示线程池初始化;
  2. 线程创建时:打印INFO日志,提示线程初始化完成;
  3. 线程启动时:打印INFO日志,提示线程开始运行;
  4. 任务入队时:打印DEBUG日志,提示任务入队成功;
  5. 任务执行时:打印DEBUG日志,提示线程获取到任务;
  6. 线程池退出时:打印INFO日志,提示线程池开始退出,线程正常退出。

线程池集成日志的示例代码

cpp 复制代码
// ThreadPool.hpp 线程池中集成日志系统
#include "Log.hpp"
using namespace LogModule;

// 线程池构造函数
ThreadPool(int thread_num = 10) : _thread_num(thread_num)
{
    LOG(LogLevel::INFO) << "ThreadPool 构造,核心线程数:" << _thread_num;
    // 初始化线程...
}

// 线程任务处理函数
void HandlerTask()
{
    std::string thread_name = "Thread-" + std::to_string(pthread_self() % 100);
    LOG(LogLevel::INFO) << thread_name << " 开始运行,等待任务";
    while (true)
    {
        // 加锁获取任务...
        LOG(LogLevel::DEBUG) << thread_name << " 获取到任务,开始执行";
        // 执行任务...
    }
}

// 任务入队函数
bool Enqueue(Task& task)
{
    // 加锁入队...
    LOG(LogLevel::DEBUG) << "任务入队成功,当前任务队列大小:" << _task_queue.size();
    return true;
}

集成后,线程池的运行过程会被全程记录 ,线上运行时只需启用文件日志,即可通过日志文件快速定位线程池的问题,比如线程启动失败任务入队异常任务执行失败等。当然我们目前还没有实现完整的线程池,后续会为大家详细介绍。

七、日志系统的扩展与优化

我们实现的日志系统是可扩展、可优化的,后续可根据实际需求进行升级,推荐的扩展方向:

  1. 新增日志策略 :比如网络日志策略 (输出到日志服务器)、数据库日志策略 (写入 MySQL/Redis),只需继承LogStrategy接口,实现SyncLog方法即可,无需修改原有代码;
  2. 实现异步日志:当前是同步日志,高并发下会有性能损耗,可实现异步日志 ------ 创建一个日志消费线程,将日志放入队列,消费线程异步写入,提升主线程性能;
  3. 日志切割 :文件日志长期运行会导致文件过大,可实现按大小 / 按时间切割日志(比如每个日志文件最大 100MB,或每天生成一个新的日志文件);
  4. 日志过滤 :根据日志等级过滤日志,比如线上环境只输出INFO及以上等级的日志,关闭DEBUG日志,减少日志量;
  5. 简化文件名__FILE__宏会返回完整路径,可通过字符串处理只保留文件名,让日志更简洁;
  6. 支持线程 ID :在日志中添加线程 IDpthread_self()),多线程环境下更易定位问题。

总结

通过本次日志系统的实现,我们可以发现:设计模式不是 "花里胡哨" 的技巧,而是解决特定问题的 "最佳实践" ,其本质是解耦------ 将变化的部分和不变的部分分离,让代码更易扩展、更易维护。

策略模式分离了策略的定义策略的实现 ,让日志的输出方式可以自由变化,而不影响日志的格式化和调用方式;RAII 机制分离了资源的申请资源的释放,让代码无需手动管理资源,避免内存泄漏和锁未释放的问题。

这些设计思想不仅适用于日志系统,也适用于线程池、网络框架等所有 C/C++ 并发开发场景,掌握这些思想,才能写出优雅、安全、可扩展的工业级代码。

后续我们会基于这个日志系统,继续实现线程池的核心功能,包括任务队列、线程管理、任务调度等,关注我,不迷路!

最后,如果你在实现过程中有任何问题,欢迎在评论区留言讨论,一起学习,一起进步!💪

相关推荐
returnthem2 小时前
Linux 测试环境完整部署手册(CentOS 7 + Ubuntu 20.04 双版本)
linux·运维·服务器
kiku18182 小时前
linux系统安全及应用
linux·运维·系统安全
进击切图仔2 小时前
linux 上编译 c++ 项目结构
linux·运维·c++
牛十二2 小时前
daily_stock_analysisA股智能分析系统源码调试使用指南
linux·运维·服务器
阿成学长_Cain2 小时前
Linux alias 命令详解:从入门到高级用法
linux·前端·chrome
s6516654962 小时前
linux-特殊符号
linux
_OP_CHEN2 小时前
【Linux系统编程】(四十七)线程安全与线程锁深度解析:从概念到实战,避坑指南全掌握
linux·操作系统·线程池·进程·线程安全·c/c++·线程锁
探序基因3 小时前
安装R包arrow
linux·运维·服务器
JiMoKuangXiangQu3 小时前
Linux 中断线程化
linux·中断线程化