【Linux】线程同步和互斥(4)线程池及手把手实现日志
目录
一 线程池
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(×tamp, &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(×tamp, &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
更多推荐




所有评论(0)