池化技术
池化技术可以减少很多的底层重复工作,例如创建进程、线程、申请内存空间时的系统调用和初始化工作。例如线程池,先预先创建好一些线程,当任务到来时直接将预先创建好的线程唤醒去处理任务,效率会远远高于任务到来时临时创建线程。例如内存池,但我们要用 1mb 空间时内存池会一次性申请 20mb 空间,效率会远远高于用多少空间申请多少空间(申请空间会调用系统调用)。 线程池是执行流级别的池化技术,STL 中的空间配置器和内存池是内存块管理级别的池化技术。
线程池的日志模块
下面开始,我们结合之前所做的所有封装,进行一个线程池的设计。在写之前,我们要做如下准备。
- 准备线程的封装
- 准备锁和条件变量的封装
- 引入日志,对线程进行封装
日志与策略模式
什么是设计模式 IT 行业这么火,涌入的人很多。俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见场景,给定了些对应的解决方案,这个就是设计模式。
什么是日志 计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。 日志格式以下几个指标是必须得有的 时间戳 日志等级(严重程度) 日志内容 以下儿个指标是可选的 文件名行号 进程,线程相关 id 信息等 日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx 等等,我们依旧采用自定义日志的方式。 这里我们采用设计模式中的策略模式来进行日志的设计,具体策略模式介绍,详情看下文。 我们想要的日志格式如下:
日志模块
两个核心问题
- 日志内容的刷新策略,刷新到显示器或文件或网络。
- 构建一条完整的日志。
设计文件等级
我们知道枚举类型中的枚举值(编译期常量)的底层存储是整数,所以我们需要把它们转换成字符串输出。(补充:枚举值的类型就是枚举类本身)
//Logger.hpp
// C++11 支持的强枚举,访问成员需指定作用域,不易出现命名冲突
enum class LogLevel {
DEBUG,
INFO, // 正常消息
WARNING, // 出现错误,但不影响程序运行
ERROR, // 导致程序退出的错误
FATAL // 重大错误
};
std::string Level_to_string(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";
}
}
刷新策略
日志的刷新策略我们打算用策略模式来设计,策略模式其实就是利用 C++ 的多态特性,先创建一个日志刷新基类,然后根据日志具体往哪刷新写具体的派生类函数,例如往显示器中写、往文件中写、往网络中写等等。
重点讲一下往文件中写,具体实现注意事项及步骤如下:
- 当我们要把日志内容刷新到文件中时需要在当前工作目录下新建一个文件夹,因为日志需要根据不同的日志等级把日志内容写到不同的文件里。
- 我们要新建文件夹首先可以用 mkdir 系统调用,但是更推荐使用 C++17 提供的文件操作接口,需要包
<filesystem>头文件。在派生类 FileLogStrategy 的构造函数里需要先将指定目录文件创建好,首先要判断在当前工作目录下指定目录文件存不存在,若存在直接返回,若存在则新建该指定目录文件,但是创建目录可能会遇到各种问题例如父目录不存在等等,所以我们创建目录时需要 try-catch 捕异常。 - 然后实现 SyncLog 往日志文件中刷新日志内容。首先我们要先拼接目标文件路径,然后使用 C++ 风格的文件操作对指定文件写入日志内容。
下面是源码及测试代码:
//logger.hpp
// 策略模式,策略接口
class LogStrategy {
public:
virtual ~LogStrategy() = default;
// 纯虚函数,强制派生类重写该函数
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 控制台日志策略,就是日志只向显示器打印,方便我们 debug
class ConsoleLogStrategy : public LogStrategy {
public:
~ConsoleLogStrategy() {}
void SyncLog(const std::string &logmessage) override {
// 显示器是共享资源,需要加锁保护
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
// 日志向文件打印
const std::string logdefaultpath = "log";
const static std::string logdefaultfilename = "test.log";
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string &dir = logdefaultpath, const std::string &filename = logdefaultfilename)
: _dir_path_name(dir), _filename(filename) {
// 有可能多个线程都在新建目录,所以最好对新建目录也进行加锁
{
LockGuard lockguard(&_lock);
if(std::filesystem::exists(_dir_path_name)) {
// 当前工作路径下目录存在,不用新建目录,直接返回
return;
}
try {
std::filesystem::create_directories(_dir_path_name);
} catch(const std::filesystem::filesystem_error &e) {
std::cerr << e.what() << '\n';
}
}
}
void SyncLog(const std::string &logmessage) override {
// 显示器是共享资源,需要加锁保护
{
LockGuard lockguard(&_lock);
// 拼接目标文件路径
std::string target = _dir_path_name;
target += "/";
target += _filename;
std::ofstream out(target.c_str(), std::ios::app); // appendif
if(!out.is_open()) {
// 打开文件失败
return;
}
out << logmessage << "\n"; // 等价于 out.write()
out.close();
}
}
~FileLogStrategy() {}
private:
std::string _dir_path_name; // 要写入的目录路径名 // log
std::string _filename; // 形成日志文件的文件名 // hello.log
// 例子:先创建一个 log 目录,再在 log 路径下形成 hello.log 文件,并把日志内容写到该文件中
Mutex _lock;
};
//main.cc
#include "Logger.hpp"
int main() {
std::string test = "hello logger";
// 测试策略 2,文件写入
// 智能指针,本质就是对 LogStrategy * 原生指针做了封装
std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();
logger_ptr->SyncLog(test);
logger_ptr->SyncLog(test);
logger_ptr->SyncLog(test);
logger_ptr->SyncLog(test);
logger_ptr->SyncLog(test);
return 0;
}
获取日志时间
- 首先需要获取时间戳,利用 time 系统调用。 参数传递 nullptr,返回值类型本质是对 long int 类型做的封装,返回值具体是 1970-01-01 午夜到现在的秒数。
- 然后根据时间戳,转化成可读性较强的时间信息,例如 1970-01-01 00:00:00。实现该功能需要利用 localtime 函数,localtime 函数有两种类型,后缀加_r 和不加_r,加_r 表示该函数可被重入,推荐使用加_r 的。 它会将时间戳转化为 struct tm 结构体,第一个是输入型参数,传递时间戳,第一个是输出型参数,将结果通过指针带出,成功返回结构体指针,失败返回 nullptr。
- 把年月日时分秒转化为字符串,我们用以前介绍过的 snprintf。 但是一点需要注意,struct tm 中的年剪掉了 1900,月是 0-11,需要我们手动把年加 1900,把月加 1。
// 根据时间戳,获取可读性较强的时间信息
std::string GetCurrentTime() {
// 1、获取时间戳
time_t curtime = time(nullptr);
// 2、把时间戳转化为年月日时分秒
struct tm currtm;
localtime_r(&curtime, &currtm);
// 3、把年月日时分秒转化为字符串
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return timebuffer;
}
logger 类实现
现在所有零件都已加工完成,接下来需要把它们拼接成一个完整的日志类,供我们使用。
首先需要激活策略,要开启什么策略,就把策略对应的派生类对象创建出来,并把派生类成员指针变量指向该对象。
class Logger {
public:
Logger() {}
// 激活策略
void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); }
void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); }
~Logger() {}
private:
std::unique_ptr<LogStrategy> _strategy;
};
现在我们还缺形成一条完整日志的方式,这里利用内部类来实现,在 logger 内部定义一个 LogMessage 的内部类,利用 LogMessage 将各种日志信息拼接成一个字符串,在 LogMessage 的析构函数中调用 logger 成员变量_strategy 的刷新函数将字符串刷新出去,这也是 RAII 风格的设计思路,所以内部类还需要定义一个对外部类引用的成员变量。
形成一条完整日志后就需要将日志信息信息刷新出去,这里我们通过重载括号运算符实现,比定义一个具体 writelog 函数更优雅,具体分析见代码注释。
内部类 LogMessage 实现
LogMessage 的构造函数:
首先将成员变量利用初始化列表初始化,然后用 stringstream 类对象 ss 将各种类型的成员变量格式化为字符串,接着调用对象 ss 的 str 接口,将其赋给_logiofo,构成日志信息的左半部分。
stringstream 需要包 <sstream> 头文件,它是 C++ 提供的格式化方案,前面我们用的 snprintf 是 C 语言提供的方案。
LogMessage 的 operator<<: 然后拼接_loginfo 右半部分,首先我们先大致看一下未来的日志输出方式:
LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c';
我们可以看到右半部分是参数是可变的,并且参数类型也不确定,所以处理思路是在内部类中对输出运算符做重载,并且参数类型设置成模板,这样任意类型参数都可以输出。
LogMessage 的析构函数: 当 LogMessage 调用 operator<< 完毕后,_loginfo 也就拼接完毕了,此时 LogMessage 要析构了,我们就可以在 LogMessage 的析构函数中将_loginfo 的日志信息刷新出去。
日志刷新流程图及源码
//logger.hpp
#pragma once
#include <iostream>
#include <filesystem> // C++17
#include <fstream>
#include <string>
#include <sstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
// C++11 支持的强枚举,访问成员需指定作用域,不易出现命名冲突
enum class LogLevel {
DEBUG,
INFO, // 正常消息
WARNING, // 出现错误,但不影响程序运行
ERROR, // 导致程序退出的错误
FATAL // 重大错误
};
std::string Level_to_string(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";
}
}
// 根据时间戳,获取可读性较强的时间信息
std::string GetCurrentTime() {
// 1、获取时间戳
time_t curtime = time(nullptr);
// 2、把时间戳转化为年月日时分秒
struct tm currtm;
localtime_r(&curtime, &currtm);
// 3、把年月日时分秒转化为字符串
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return timebuffer;
}
///////////////////////////////////////////////////////////////
// 策略模式,策略接口
class LogStrategy {
public:
virtual ~LogStrategy() = default;
// 纯虚函数,强制派生类重写该函数
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 控制台日志策略,就是日志只向显示器打印,方便我们 debug
class ConsoleLogStrategy : public LogStrategy {
public:
~ConsoleLogStrategy() {}
void SyncLog(const std::string &logmessage) override {
// 显示器是共享资源,需要加锁保护
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
// 日志向文件打印
const std::string logdefaultpath = "log";
const static std::string logdefaultfilename = "test.log";
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string &dir = logdefaultpath, const std::string &filename = logdefaultfilename)
: _dir_path_name(dir), _filename(filename) {
// 有可能多个线程都在新建目录,所以最好对新建目录也进行加锁
{
LockGuard lockguard(&_lock);
if(std::filesystem::exists(_dir_path_name)) {
// 当前工作路径下目录存在,不用新建目录,直接返回
return;
}
try {
std::filesystem::create_directories(_dir_path_name);
} catch(const std::filesystem::filesystem_error &e) {
std::cerr << e.what() << '\n';
}
}
}
void SyncLog(const std::string &logmessage) override {
// 显示器是共享资源,需要加锁保护
{
LockGuard lockguard(&_lock);
// 拼接目标文件路径
std::string target = _dir_path_name;
target += "/";
target += _filename;
std::ofstream out(target.c_str(), std::ios::app); // appendif
if(!out.is_open()) {
// 打开文件失败
return;
}
out << logmessage << "\n"; // 等价于 out.write()
out.close();
}
}
~FileLogStrategy() {}
private:
std::string _dir_path_name; // 要写入的目录路径名 // log
std::string _filename; // 形成日志文件的文件名 // hello.log
// 例子:先创建一个 log 目录,再在 log 路径下形成 hello.log 文件,并把日志内容写到该文件中
Mutex _lock;
};
////////////////////////////////////////////////////////////
class Logger {
public:
Logger() {}
// 激活策略
void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); }
void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); }
// 形成一条完整的日志信息,利用内部类 LogMessage
class LogMessage {
public:
LogMessage(LogLevel level, std::string filename, int line, Logger &logger)
: _curr_time(GetCurrentTime()), _level(level), _pid(getpid()),
_filename(filename), _line(line), _logger(logger) {
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level_to_string(_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() {
// 如果外部类的_strategy 的成员变量不为空,
// 就可以将_loginfo 的内容通过_strategy 刷新出去
if(_logger._strategy) {
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 日志时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程 pid
std::string _filename; // 对应的文件名
int _line; // 对应的文件行号
std::string _loginfo; // 一条合并完成的,完整的日志信息
Logger &_logger; // 对外部类引用,利用 Logger 为 LogMessage 提供刷新策略
};
// Logger 提供的写日志方法
LogMessage operator()(LogLevel level, std::string filename, int line) {
// 返回 LogMessage 的匿名对象
return LogMessage(level, filename, line, *this);
}
// 上面 LogMessage operator() 实现引出的一些知识点:
// 1、直接返回 LogMessage 临时对象,和先显式创建对象再返回的写法
// 在绝大多数场景下效果完全一致,返回临时对象的写法
// 编译器会直接在'接收返回值的位置'构造 LogMessage 对象,无任何拷贝/移动
// 先显式创建对象再返回的写法编译器会触发 RVO 优化,同样省略 log_msg 的拷贝 / 移动,
// 最终效果和写法 1 完全一致;即使关闭优化,C++11 也会通过移动构造函数
// (LogMessage 无自定义移动构造时,编译器自动生成)完成返回,性能损耗可忽略。
// 2、的生命周期并非简单的'只有一行',它的存活时间会根据上下文被编译器延长,
// 尤其是在函数返回的场景下,例如上面代码的 LogMessage 匿名对象
// 3、这里写日志方法完全可以不重载括号运算符,而是写成一个具体函数如 WriteLog,
// 这样就只在:#define LOG(level) logger.WriteLog(level, __FILE__, __LINE__)
// 需要修改一下调用方法,而重载括号运算符的会更优雅,推荐~
~Logger() {}
private:
std::unique_ptr<LogStrategy> _strategy;
};
// 先定义一个 Logger 对象,后面定义宏会用到
Logger logger;
// 定义调用日志的宏
#define LOG(level) logger(level, __FILE__, __LINE__)
// 定义选择日志模式的宏(也就是选择日志刷新策略)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
//main.cc
#include "Logger.hpp"
#include <unistd.h>
int main() {
// 预处理阶段时 EnableConsoleLogStrategy() 会被宏替换为 logger.EnableConsoleLogStrategy()
EnableConsoleLogStrategy();
// EnableFileLogStrategy();
LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';
LOG(LogLevel::WARNING) << "hello world" << 1234 << ", 3.14 " << 'c';
LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';
LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';
// std::string test = "hello logger";
// // 测试策略 1,显示器写入
// // 智能指针,本质就是对 LogStrategy * 原生指针做了封装
// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();
// logger_ptr->SyncLog(GetCurrentTime());
// sleep(1);
// logger_ptr->SyncLog(GetCurrentTime());
// sleep(1);
// logger_ptr->SyncLog(GetCurrentTime());
// sleep(1);
// logger_ptr->SyncLog(GetCurrentTime());
// sleep(1);
// logger_ptr->SyncLog(GetCurrentTime());
// sleep(1);
// // 测试策略 2,文件写入
// // 智能指针,本质就是对 LogStrategy * 原生指针做了封装
// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();
// logger_ptr->SyncLog(test);
// logger_ptr->SyncLog(test);
// logger_ptr->SyncLog(test);
// logger_ptr->SyncLog(test);
// logger_ptr->SyncLog(test);
return 0;
}


