目录

一 线程池

1 什么是线程池

2 日志认识

3 实现日志


一 线程池

1 什么是线程池

提前创建的一批专门用来处理任务的线程,目的是提高效率。

用户有任何任务,交给线程池,线程池会自动的帮你处理--->线程池本质就是多生产者多消费者模型

可以给每个线程都给一个任务队列,负载均衡的把任务交给任务队列

所以之前写的基于管道的进程池也是生产者消费者模型

2 日志认识

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

日志是有等级的,不同等级的日志有不同的响应动作

DEBUG:开发调试用
INFO:正常运行普通记录
WARN:有隐患但不崩程序
ERROR:功能出错,程序还能运行
FATAL:致命错误,程序直接崩溃

日志格式以下几个指标是必须有的: • 时间戳 • 日志等级 • 日志内容

以下几个指标是可选的: • 文件名行号 • 进程、线程相关ID信息等

我们想要的日志格式如下:

[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持
可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

日志就是形成一个字符串,具有debug的借鉴功能-->日志形成的两个阶段:(1)形成日志(2)刷新日志

我们采用一种策略模式来设计:根据不同的策略,来选择日志的输出地点--->显示器或文件

策略模式:把不同的“算法/行为”分别封装成类,运行时自由切换用哪一个,不用修改主业务代码

3 实现日志


获取时间

#include <time.h>
time_t time(time_t *timer);

函数作用:返回1970-1-1到现在的时间秒数,就是时间戳,要是想显示年-月-日-时-分-秒,是想把时间戳转化为具体的时间

lockaltime_r:

struct tm *localtime_r(const time_t *timep, struct tm *result);

第一个参数是传入时间戳的地址,第二个参数是struct tm--->把时间戳转化为具体的年-月-日

注意:struct tm的年份是从1900开始的,所以需要给年份加上1900;而月是从0开始的,所以要加1

除了年份以外,其余都是两位数 所以格式为:%4d(年),不足两位的补0:%02d(月,日……)

所以获取时间部分代码如下:

 std::string GetTimeStamp()
     {
         time_t timestamp = time(nullptr);
         struct tm data_time;
         localtime_r(&timestamp, &data_time);
 
         char data_time_str[128];
         snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
                  data_time.tm_year + 1900,
                  data_time.tm_mon + 1,
                  data_time.tm_mday,
                  data_time.tm_hour,
                  data_time.tm_min,
                  data_time.tm_sec);
 
         return data_time_str;
     }

我们可以使用枚举来列举日志的等级:enum class ,是C++11的强类型枚举。class既是类型,也是常量,作为public访问,使用时要加上作用域,作用域是枚举的名称

枚举的本质是整数,打印时是0,1,2……,但是我们想要显示状态:ERROR……;所以需要一个接口,将枚举转化为字符串--->std::string LogLevel25string(LogLevel level)

所以我们就得到了日志等级的部分代码:
 

 enum class LogLevel
     {
         DEBUG,
         INFO,
         WARNING,
         ERROR,
         FATAL
     };
     // 2. 日志等级
     std::string LogLevel2String(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";
         }
     }

日志消息就是字符串,设计策略模式,就是指向不同的对象实现多样化输出

具体实现方式:
基于继承实现,定义抽象基类,提供统一的纯虚接口
不同子类重写接口,实现各自独立的日志输出策略
必须添加线程锁保护:日志刷新策略会被多线程并发调用,防止数据混乱

往文件里打印,需要知道:(1)文件名 (2)文件路径

在当前程序目录下创建指定的日志路径 logdir,创建前先判断路径是否存在:

if (!std::filesystem::exists(_logdir))

我们先来写基类:主要是用来制定刷新策略

析构函数必须是纯虚的--->要从子类析构;刷新日志-->刷新字符串

 class LogStrategy
     {
     public:
         virtual ~LogStrategy() = default;
         virtual void SyncLog(const std::string &logmessage) = 0;
     };

注意:基类的纯虚函数必须要在子类中实现

将来基类指针指向子类对象的时候,调用刷新策略Syncllog的时候,打印的时候就可以向特定的位置打

我们下面来写子类:
(1)往显示器打印:将来使用这个日志的时候,可能会有很多个进程或线程访问,就会有多个进程往显示器打印,但是显示器本身就是临界资源,显示器需要被保护且打印的过程是原子的--->加锁

显示器写入,本质就是追加写入,将日志字符串追加到目标位置

 // 子类:继承纯虚接口类
     // 策略1
     class ConsoleLogStrategy : public LogStrategy
     {
     public:
         ConsoleLogStrategy() {}
         ~ConsoleLogStrategy() {}
         void SyncLog(const std::string &logmessage) override
         {
             LockGuard lockguard(&_mutex);
             std::cout << logmessage << std::endl;
         }
 
     private:
         Mutex _mutex;
     };

(2)往文件打印:往文件打印,首先要知道对应日志文件的工作路径--->std::string _longdir;还有在指定工作目录下的日志文件名--->sdi::string _logfilename;

如果我们当前文件夹下不存在该目录和文件,则自动创建该目录和文件--->在C++17中,filesystem文件库,当中的exists就是判断当前目录是否存在,如果存在,什么都不做;如果不存在则创建

检测目录是否存在和创建目录本身也是线程安全的--->加锁

ofstream:以追加模式 打开文件,把日志字符串写进

std::ofstream ofs("log/log.txt", std::ios::app);

static const std::string glogdir = "./log/";
     static const std::string glogfilename = "log.txt";//默认设置的路径和文件名


// 子类:继承纯虚接口类
// 策略2
     class FileLogStrategy : public LogStrategy
     {
     public:
         FileLogStrategy(const std::string &dir = glogdir, const std::string &filename = glogfilename)
             : _logdir(dir), _logfilename(filename)
         {
             // log / log.txt
             LockGuard lockguard(&_mutex);
             if (std::filesystem::exists(_logdir))
             {
                 return;
             }
             else
             {
                 try//不存在就创建目录
                 {
                     std::filesystem::create_directories(_logdir);
                 }
                 catch (const std::filesystem::filesystem_error &e)//可能会出错:捕捉异常
                 {
                     std::cerr << e.what() << '\n';
                 }
             }
         }
         ~FileLogStrategy()
         {
         }
         void SyncLog(const std::string &logmessage) override
         {
             LockGuard lockguard(&_mutex);
             std::string target = _logdir + _logfilename;//文件=文件路径+文件名
             std::ofstream out(target, std::ios::app); // 追加写入文件
             if (!out.is_open())//打开文件没有成功
             {
                 return;
             }
             out << logmessage << "\n";
             out.close();
         }
 
     private:
         std::string _logdir;
         std::string _logfilename; // ./log/XXX.log
         Mutex _mutex;
     };

class logger是我们真正要的日志类:要给我们提供刷新日志的策略,刷新日志的策略是可以随时被更改的

我们在class logger中,设计一个内部类-->就是一条日志,目标是把一个类对象,变成一个string

日志要有:

我们也想知道这个日志是在哪个文件的哪一行被打印出来的--->int _line;

日志是会被分成两个部分:左半部分-->参数化,可直接获取;右半部分--->可变参数部分

stringstream:对字符串流处理的类。作用是把各种类型数据(数字、字符串、时间、日志等级)像 cout 一样拼接,最后一次性转成 std::string

右半部分处理:
在logger类中,提供一个仿函数--->operator()(),打印日志时,会调用仿函数,以仿函数的方式,返回一个临时的logMessage内部类的对象,而临时对象重载了 <<符号,在内部就会拼接成字符串

在内部类中(LogMessage)增加一个:logger&self,相当于把外部类传给内部类

我们使用RAII风格的日志刷新!

~LogMessage() // RAII风格的日志刷新!
{
    // 如果日志策略存在(不为空)
    if (_logger._strategy)
    {
        // 调用策略的同步输出方法,把日志字符串刷出去
        _logger._strategy->SyncLog(_loginfo);
    }
}

当临时日志对象生命周期结束(比如一行日志写完),自动调用析构函数,自动输出日志

C/C++中的两个预处理符:_FILE_   ,_LINE_    在编译时由预处理器自动替换为对应的信息

__FILE__:预定义宏,编译时会被替换成当前代码文件的路径 / 文件名(字符串形式)
__LINE__:预定义宏,编译时会被替换成当前代码所在的行号(整数形式)

例如:

std::cout << __FILE__ << " : " << __LINE__ << std::endl;

会在在控制台打印出:当前代码的文件名 + 当前行号

使用宏,包装我们的日志打印过程,宏有一个特点,#define A B, B替换成为A。在任意地方使用A,就会被替换成B

#define LOG(level) logger(level, __FILE__, __LINE__)

例如:原来要写(麻烦)

logger(DEBUG, "main.cpp", 42);

现在写:

LOG(DEBUG);

宏会自动帮你替换成:

logger(DEBUG, "main.cpp", 42);

动态调整日志策略:

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
 #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy();

一行代码,就可以快速切换日志输出到:控制台(屏幕) or   文件(log.txt)

至此,我们日志部分的代码就全部完成,下面是完整代码:

#ifndef __LOGGER_HPP
 #define __LOGGER_HPP
 
 #include <iostream>
 #include <cstdio>
 #include <string>
 #include <ctime>
 #include <filesystem> // C++17
 #include <fstream>
 #include <sstream>
 #include <memory>
 #include <unistd.h>
 #include "Mutex.hpp"
 
 namespace LogModule
 {
     // 1. 获取时间
     std::string GetTimeStamp()
     {
         time_t timestamp = time(nullptr);
         struct tm data_time;
         localtime_r(&timestamp, &data_time);
 
         char data_time_str[128];
         snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
                  data_time.tm_year + 1900,
                  data_time.tm_mon + 1,
                  data_time.tm_mday,
                  data_time.tm_hour,
                  data_time.tm_min,
                  data_time.tm_sec);
 
         return data_time_str;
     }
 
     enum class LogLevel
     {
         DEBUG,
         INFO,
         WARNING,
         ERROR,
         FATAL
     };
     // 2. 日志等级
     std::string LogLevel2String(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";
         }
     }
     // 3. 日志刷新
     // 基类:策略基类, 设置刷新策略的
     class LogStrategy
     {
     public:
         virtual ~LogStrategy() = default;
         virtual void SyncLog(const std::string &logmessage) = 0;
     };
 
     // 子类:继承纯虚接口类
     // 策略1
     class ConsoleLogStrategy : public LogStrategy
     {
     public:
         ConsoleLogStrategy() {}
         ~ConsoleLogStrategy() {}
         void SyncLog(const std::string &logmessage) override
         {
             LockGuard lockguard(&_mutex);
             std::cout << logmessage << std::endl;
         }
 
     private:
         Mutex _mutex;
     };
 
     static const std::string glogdir = "./log/";
     static const std::string glogfilename = "log.txt";
     // 子类:继承纯虚接口类
     // 策略2
     class FileLogStrategy : public LogStrategy
     {
     public:
         FileLogStrategy(const std::string &dir = glogdir, const std::string &filename = glogfilename)
             : _logdir(dir), _logfilename(filename)
         {
             // log / log.txt
             LockGuard lockguard(&_mutex);
             if (std::filesystem::exists(_logdir))
             {
                 return;
             }
             else
             {
                 try
                 {
                     std::filesystem::create_directories(_logdir);
                 }
                 catch (const std::filesystem::filesystem_error &e)
                 {
                     std::cerr << e.what() << '\n';
                 }
             }
         }
         ~FileLogStrategy()
         {
         }
         void SyncLog(const std::string &logmessage) override
         {
             LockGuard lockguard(&_mutex);
             std::string target = _logdir + _logfilename;
             std::ofstream out(target, std::ios::app); // 追加写入文件
             if (!out.is_open())
             {
                 return;
             }
             out << logmessage << "\n";
             out.close();
         }
 
     private:
         std::string _logdir;
         std::string _logfilename; // ./log/XXX.log
         Mutex _mutex;
     };
 
     // 我们真正要的,日志类
     class Logger
     {
     public:
         Logger()
         {
             UseConsoleLogStrategy();
         }
         ~Logger()
         {
         }
         void UseConsoleLogStrategy()
         {
             _strategy = std::make_unique<ConsoleLogStrategy>();
         }
         void UseFileLogStrategy()
         {
             _strategy = std::make_unique<FileLogStrategy>();
         }
 
         // 内部类: 一条日志
         // 目标是把一个类对象,变成一个string
         class LogMessage
         {
         public:
             LogMessage(LogLevel level, std::string &filename, int line, Logger &self)
                 : _level(level),
                   _curr_time(GetTimeStamp()),
                   _pid(getpid()),
                   _filename(filename),
                   _line(line),
                   _logger(self)
             {
                 std::stringstream ss;
                 ss << "[" << _curr_time << "] "
                    << "[" << LogLevel2String(_level) << "] "
                    << "[" << _pid << "] "
                    << "[" << _filename << "] "
                    << "[" << _line << "] "
                    << "- ";
                 _loginfo = ss.str();
             }
 
             template <typename T>
             LogMessage &operator<<(const T &info)
             {
                 std::stringstream ss;
                 ss << info;
                 _loginfo += ss.str();
                 return *this;
             }
 
             ~LogMessage() // RAII风格的日志刷新!
             {
                 if (_logger._strategy)
                 {
                     _logger._strategy->SyncLog(_loginfo);
                 }
             }
 
         private:
             LogLevel _level; //  日志等级
 
             std::string _curr_time; // 当前时间
             pid_t _pid;             // 进程pid
             std::string _filename;  // 文件名
             int _line;              // 行号
             std::string _loginfo;   // 一条完整的日志
 
             Logger &_logger; // 外部类的引用
         };
 
         // Logger 对象打印日志的时候,故意返回一个临时的LogMessage对象
         // 为什么要返回临时内部类对象??
         LogMessage operator()(LogLevel level, std::string filename, int line)
         {
             return LogMessage(level, filename, line, *this);
         }
 
     private:
         std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
     };
 
     Logger logger;
 
 // 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B, B替换成为A
 #define LOG(level) logger(level, __FILE__, __LINE__)
 
 // 动态调整日志策略
 #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
 #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy();
 }
 
 #endif

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐