跳到主要内容 手写高性能日志模块:基于策略模式与线程安全设计 | 极客日志
C++
手写高性能日志模块:基于策略模式与线程安全设计 在 Linux 环境下使用 C++ 手写高性能日志模块的实现方案。内容涵盖池化技术概念、日志系统核心要素(等级、时间戳、文件行号)、基于策略模式的输出方式(控制台与文件),以及利用运算符重载实现灵活日志拼接的 Google 风格设计。代码示例展示了线程安全的日志类结构、目录自动创建及刷新机制,为后续线程池开发奠定基础。
林间仙子 发布于 2026/2/9 更新于 2026/4/18 1 浏览谈兵先识器:在构建线程池之前,我们先聊聊'池化技术'
在我们掌握了线程控制、锁以及条件变量这些并发编程的利器之后,我们终于可以着手设计并实现一个真正实用的组件——线程池。
但在我们敲下第一行代码之前,有一个更宏观、更具指导性的思想需要先深入理解,那就是池化技术 (Pooling Technology)。这不仅是线程池的核心,也是构建几乎所有高性能后台服务的基石。
什么是池化技术?—— 一种'未雨绸缪'的智慧
让我们先抛开代码,来看一个生活中的例子:
想象一下,你经营着一家非常火爆的网约车公司。每当有乘客下单时,你才开始打电话招募司机、给他们注册、分配车辆。等这一套流程走完,乘客早已不耐烦地取消了订单。
聪明的做法是什么?你提前招募并培训好一批司机,让他们在几个热门地段的'司机站'里随时待命。订单一来,你立刻从站里派一位空闲的司机出发。任务完成后,司机不是解雇回家,而是返回站点继续等待下一个订单。
这个'司机站'就是'池 '。池化技术的核心思想就是:将一批昂贵的、需要频繁使用的资源预先创建好并统一管理起来,当需要时直接从'池'中获取,用完后不是销毁,而是归还给'池',以供后续复用。
为什么要池化?—— 因为'从零创建'的代价远比你想象的要高
正如图表所示,池化技术旨在'减少底层重复工作 '。
然而,如果我们直接一头扎进复杂的并发代码中,就好比在没有地图和手电筒的情况下探索一个漆黑的洞穴——我们很快就会迷失方向。
这个'手电筒'和'地图',就是日志系统 。在多线程环境中,断点调试(GDB)的作用会因为线程间的时序和调度问题而大打折扣。一个可靠的、能够记录关键信息和错误状态的日志系统,是我们分析、调试和监控我们未来线程池运行状态的生命线。
但在动手写日志系统之前,我们先要学习一种能让它变得无比灵活和强大的设计模式——策略模式 (Strategy Pattern) 。
日志与策略模式
为了讲解日志模式,这里不得不提到设计模式。IT 行业这么火,涌入的人很多。俗话说林子大了什么鸟都有。大佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见场景,给定了一些对应的解决方案,这个就是设计模式。在编写过程中会参考业界主流日志框架的设计思路。
日志认识
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
那么一个日志有哪些必要的指标呢?格式应该是怎样的呢?
必要:
时间戳(记录出现问题时的时间)
日志内容(我报的内容是啥错?)
日志等级(这个问题是正常输出的;是申请失败,不影响运行;还是引起错误,可以重新重启的呢?)
DEBUG:做测试
INFO:正常输出
WARNING:申请失败,不影响运行
ERROR:引起错误,可以重新重启
FATAL:不一定是正常结束,需要排查
可选:
文件名和行号
进程,线程相关 id 信息等
日志格式示例:
[可读性很好的时间] [日志等级] [进程 pid] [打印对应日志的文件名] [行号] - 消息内容,支持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] - hello world
- hello world
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
[16]
[2024-08-04 12:27:03]
[WARNING]
[202938]
[main.cc]
[23]
代码编写 进行日志格式代码设计的时候,需要注意以下两个核心问题:
刷新策略 --> 是向显示器刷新,还是文件刷新,还是网络等等呢?
需要刷新那么就一定得构建一条完整的日志,而这需要日志格式的支持。
日志等级
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
std::string Level2String (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" ;
}
}
这里通过枚举分为 5 个日志等级,那为什么还需要 Level2String 这个函数呢?
DEBUG 等在编译后就是数字 ,为了将枚举值转换为人类可读的字符串,因此用 Level2String 函数将数字转为字符串。
时间戳 系统提供一个获取时间戳的系统调用:time_t time(time_t *tloc); tloc 指的是时区,默认为 nullptr 即可。time_t 是什么呢?long int 一个长整数。
time_t currtime = time (nullptr );
可是时间戳我们看不懂啊?同样需要转为人类看的时间,提供了一套函数把时间戳转换成为人类看得懂的语言。
struct tm *localtime (const time_t *timep);
struct tm *localtime_r (const time_t *restrict timep, struct tm *restrict result);
timep 是输入参数;restrict result 是输出参数。
这两个函数都将一个指向 time_t 类型(通常是自 1970-01-01 00:00:00 +0000 (UTC) 以来的秒数)的指针作为输入,并将其转换为本地时间(根据系统设定的时区进行计算),然后将转换后的结果填充到一个 struct tm 结构体中。
带 _r 是什么意思呢?凡是带 _r 的代表该函数是可被重入的。
struct tm {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
long tm_gmtoff;
const char *tm_zone;
};
struct tm currtm;
localtime_r (&currtime, &currtm);
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);
文件名和行号 #define LOG(level) logger(level, __FILE__, __LINE__)
__FILE__:自动替换为当前源代码文件的文件名(字符串类型);__LINE__:自动替换为当前代码所在的行号(整数类型)。
刷新策略 假设这时已经拿到了一条完整的日志,而这条日志是向显示器刷新还是向文件刷新还是网络刷新?由我们进行选择,因此我们必须要写一个关于刷新策略的类。这里通过定义:
LogStrategy 的基类 -- 定义统一的日志行为,定义所有日志输出策略必须实现的核心方法,确保不同输出方式的接口一致性。
class LogStrategy {
public :
virtual ~LogStrategy () = default ;
virtual void SyncLog (const std::string &logmessage) = 0 ;
};
ConsoleLogStrategy —— 控制台日志输出
将来我们的日志可以被多种线程访问的,如果刷新测量是往显示器做刷新,一旦刷新往显示器上打,显示器作为临界资源就需要被保护起来,因此需要进行加锁啊!
class ConsoleLogStrategy : public LogStrategy {
public :
~ConsoleLogStrategy () {}
void SyncLog (const std::string &logmessage) override {
{
LockGuard lockguard (&_lock) ;
std::cout << logmessage << std::endl;
}
}
private :
Mutex _lock;
};
FileLogStrategy —— 文件日志输出
当刷新策略设定为向文件刷新日志时,自然会想到对日志信息按等级分类 —— 比如哪些属于 INFO 级、哪些是 ERROR 级、哪些又该归为 FATAL 级,再将不同等级的日志分别写入对应文件中。但随之会遇到一个问题:若把这些日志文件都直接放在当前目录下,随着时间推移,文件会越积越多,整个目录会显得杂乱无章。所以很自然地会考虑创建一个专门的 log 目录,用来统一存放这些日志文件。
C++17 提供了文件操作 --> 判断目录是否存在,不存在就创建。
std::filesystem::exists (_dir_path_name);
std::filesystem::create_directories (_dir_path_name);
const std::string logdefaultdir = "log" ;
const static std::string logfilename = "test.log" ;
class FileLogStrategy : public LogStrategy {
public :
FileLogStrategy (const std::string &dir = logdefaultdir, const std::string filename = logfilename)
: _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 () << "\r\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) ;
if (!out.is_open ()) {
return ;
}
out << logmessage << "\n" ;
out.close ();
}
}
~FileLogStrategy () {}
private :
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
激活策略 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;
};
日志内容 实现日志信息的方法有很多种,这里介绍一种谷歌实现日志的十分巧妙的方法:
首先定义了一个内部类 LogMessage,为了能支持将一条日志信息刷新出去,将外部类的刷新策略也带进内部类里,这样一旦形成日志信息,就可以把数据刷新出去。
class LogMessage {
private :
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger;
};
LOG (LogLevel::ERROR) << "hello world" << ", 3.14 " << 123 ;
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 << "] "
<< "[" << Level2String (_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 operator () (LogLevel level, std::string filename, int line) {
return LogMessage (level, filename, line, *this );
}
LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;
会被宏替换为 logger(LogLevel::ERROR, __FILE__, __LINE__) << "hello world" << ", 3.14 " << 123;
接着 logger(LogLevel::ERROR, __FILE__, __LINE__) 会调用 () 运算符重载,故意拷贝,形成 LogMessage 临时对象,拿到了除日志内容的相关信息,需要注意的是 () 重载返回的是一个临时对象啊!后续在被 << 时,会被持续引用,直到没有 <<,_loginfo 已经有一条完整的信息了。此时还差一口气没有补上,如何把这条日志信息刷新出去呢?由于该临时对象没被 << 时,生命周期结束了,此时会被析构啊!那么我们在析构函数这将信息刷新出去不就行了?
总结 本文介绍了 Linux 日志系统的设计与实现,重点讲解了日志格式设计和刷新策略。主要内容包括:
日志等级划分(DEBUG/INFO/WARNING/ERROR/FATAL)及对应字符串转换;
时间戳获取与格式化;
基于策略模式的日志输出方式(控制台输出、文件输出等);
采用 Google 风格的日志内容构建方法,通过运算符重载实现灵活的日志信息拼接。文章还详细阐述了文件日志策略中的目录创建、线程安全处理等关键技术点,最终实现了一个支持多级别、多输出方式的灵活日志系统框架。
附源码 #pragma once
#include <iostream>
#include <string>
#include <filesystem>
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
std::string Level2String (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 () {
time_t currtime = time (nullptr );
struct tm currtm;
localtime_r (&currtime, &currtm);
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 ;
};
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 logdefaultdir = "log" ;
const static std::string logfilename = "test.log" ;
class FileLogStrategy : public LogStrategy {
public :
FileLogStrategy (const std::string &dir = logdefaultdir, const std::string filename = logfilename)
: _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 () << "\r\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) ;
if (!out.is_open ()) {
return ;
}
out << logmessage << "\n" ;
out.close ();
}
}
~FileLogStrategy () {}
private :
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
class Logger {
public :
Logger () {}
void EnableConsoleLogStrategy () { _strategy = std::make_unique <ConsoleLogStrategy>(); }
void EnableFileLogStrategy () { _strategy = std::make_unique <FileLogStrategy>(); }
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 << "] "
<< "[" << Level2String (_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 () {
if (_logger._strategy) {
_logger._strategy->SyncLog (_loginfo);
}
}
private :
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger;
};
LogMessage operator () (LogLevel level, std::string filename, int line) {
return LogMessage (level, filename, line, *this );
}
~Logger () {}
private :
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()