在模拟实现线程池之前,我们先来实现一个简单的日志,在这之前我们需要铺垫一些概念
一、核心定义:什么是设计模式?
设计模式是在软件设计中,针对特定场景的常见问题的、可重用的解决方案。它并不是一个可以直接转换成代码的完整设计,而更像是一个模板或蓝图,指导你如何优雅地解决一类问题。
你可以把它想象成:
二、为什么需要设计模式?(目的与好处)
三、设计模式的三大分类
经典著作《设计模式:可复用面向对象软件的基础》中将23种常见模式分为三类:
1. 创建型模式
关注点:如何创建对象。 它们将对象的创建过程抽象出来,使得系统与对象的创建、组合方式解耦。
Button类,其子类WindowsButton和MacButton由不同的工厂创建。
Computer对象,CPU、内存、硬盘等部件可以灵活组合,而不是在构造函数中传入无数参数。
2. 结构型模式
关注点:如何组合类和对象以形成更大的结构。 它们通过继承和组合,来构建灵活、高效的程序结构。
3. 行为型模式
关注点:对象之间的职责分配和通信。 它们主要负责管理算法、职责和对象间的交互。
四、重要原则:SOLID原则
设计模式背后是面向对象设计的核心原则,最著名的就是SOLID原则:
五、如何使用设计模式?(忠告)
计算机中的日志文件是记录系统和应用程序运行过程中发生事件的重要数据载体。它们以时间序列的方式详细记录系统状态、用户操作、异常情况等关键信息。日志的主要作用体现在三个方面:一是实时监控系统运行状态,帮助运维人员了解系统健康状况;二是记录异常和错误信息,为后续故障排查提供依据;三是支持安全审计,通过分析用户行为和系统事件来发现潜在安全隐患。
在日志格式规范方面,通常会包含以下核心要素:
可选的高级指标包括:
在日志系统实现方面,常见的成熟解决方案有:
虽然这些现成方案功能完善,但考虑到特定项目需求,我们决定采用自定义日志系统的设计。为此,我们选择使用设计模式中的策略模式来构建灵活的日志系统架构:
策略模式的实现思路:
这种设计允许在不修改现有代码的情况下,灵活扩展新的日志输出方式,同时保持日志格式的统一性。例如,在开发阶段可以使用控制台日志便于调试,而在生产环境可以无缝切换到文件日志或网络日志。
我们想要的日志格式如下:
[可读性很好的时间] [⽇志等级] [进程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我们先来实现具体的策略类,这里使用两种日志策略,分别是控制台日志策略和文件日志策略
代码如下:
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include <filesystem> // C++17
#include <fstream>
#include <cstdio>
#include <string>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 策略模式
// 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类——抽象类
class LogStrategy
{
public:
~LogStrategy() = default; // 本质也是虚函数,多态行为,防止资源泄漏
virtual void SyncLog(const std::string& message) = 0; // 纯虚函数
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy() {}
void SyncLog(const std::string& message) override
{
LockGuard lockguard(_mutex);
std::cout << message << std::endl;
}
~ConsoleLogStrategy() {}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
// 文件打印日志的策略 : 子类
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string& path = defaultpath, const std::string& file = defaultfile)
:_path(path), _file(file)
{
LockGuard lockguard(_mutex);
// 判断文件路径存不存在
if(std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path); // 不存在则创建路径
}
catch(const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n'; // 失败就抛异常,并捕获
}
}
void SyncLog(const std::string& message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app); // 以追加的方式打开文件
if(!out.is_open())
{
return; // 打开失败就返回
}
out << message << gsep;
out.close();
}
~FileLogStrategy() {}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件
Mutex _mutex;
};
}
#endif1. 总体架构 (策略模式核心)
LogStrategy):定义所有具体策略必须实现的通用接口 SyncLog。这是多态的基础,允许上下文(未来的 Logger 类)依赖抽象,而非具体实现。
ConsoleLogStrategy, FileLogStrategy):继承自抽象接口,并提供了接口方法的不同实现。一个将日志输出到控制台,另一个则输出到文件。
这种结构使得增加新的日志输出策略(例如输出到网络、数据库、Syslog等)变得非常容易,只需创建一个新的类继承 LogStrategy 并实现 SyncLog 方法即可,完全符合 “开闭原则”。
2. 代码结构分解
a. 抽象基类:LogStrategy
class LogStrategy {
public:
~LogStrategy() = default; // 关键:虚析构函数确保派生类正确释放
virtual void SyncLog(const std::string& message) = 0; // 纯虚函数,定义策略契约
};SyncLog:强制所有子类必须实现这个日志同步输出方法。
b. 具体策略A:ConsoleLogStrategy (输出到标准输出)
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() {} // 构造函数通常不需要特殊操作
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex); // 线程安全关键!
std::cout << message << std::endl; // 核心操作:输出到控制台
}
~ConsoleLogStrategy() {}
private:
Mutex _mutex; // 互斥锁,保证多线程下输出不混乱
};LockGuard 和 Mutex 确保在多线程环境下,多个线程同时调用 SyncLog 时,日志消息不会交叉重叠,输出是完整的。这是生产级代码的重要特征。
c. 具体策略B:FileLogStrategy (输出到文件)
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string& path = defaultpath, const std::string& file = defaultfile)
:_path(path), _file(file) {
LockGuard lockguard(_mutex);
// 构造时检查并创建日志目录
if(!std::filesystem::exists(_path)) {
try {
std::filesystem::create_directories(_path);
} catch(const std::filesystem::filesystem_error &e) {
std::cerr << e.what() << '\n'; // 异常处理:目录创建失败
}
}
}
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex); // 线程安全
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app); // 核心:以追加模式打开文件
if(!out.is_open()) {
return; // 处理文件打开失败
}
out << message << gsep; // 写入文件并换行
out.close(); // 显式关闭文件(也可依赖析构函数自动关闭)
}
private:
std::string _path;
std::string _file;
Mutex _mutex; // 保证多线程写文件安全
};std::ofstream 以追加模式 (std::ios::app) 打开文件,确保不会覆盖历史日志。
'/'。
注意:上面代码中我们使用了<filesystem> 库。下面我们来简单介绍一下C++17 引入的 <filesystem> 库:
什么是 <filesystem> 库?
C++ <filesystem> 库(正式名称为 std::filesystem)是 C++ 标准库的一部分,它提供了一套用于操作文件系统路径、目录和文件的类、函数和常量。
在它出现之前,C++ 程序员必须依赖操作系统特定的 API(如 Windows 的 <windows.h> 或 Linux 的 <unistd.h>)或第三方库(如 Boost.Filesystem)来进行文件系统操作。<filesystem> 库将这些功能标准化,使得编写跨平台的文件操作代码变得非常简单和便捷。
它的核心功能是什么?
<filesystem> 库主要帮助你处理以下几类任务:
path p = "/home/user/logs"; p /= "app.log"; // 路径变为 /home/user/logs/app.log
exists)。
is_directory, is_regular_file)。
create_directory, create_directories)。
copy)。
rename)。
remove, remove_all)。
我们来测试一下效果:
#include "Log.hpp"
using namespace LogModule;
int main()
{
std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>();
//std::unique_ptr<LogStrategy> strategy = std::make_unique<FileLogStrategy>();
strategy->SyncLog("hello log!");
return 0;
}先来测试一下控制台日志策略,将日志输出到控制台上,运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./test
hello log!再来试一下文件日志策略,将日志输出到文件中,运行结果:

可以看到在当前路径中创建了一个log目录,并在log目录下创建了my.log文件,且日志也输出到了文件中
代码如下:
// 日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToStr(const 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 GetTimeStamp()
{
time_t cur = time(nullptr);
struct tm curr_tm;
localtime_r(&cur, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900, curr_tm.tm_mon + 1, curr_tm.tm_mday, curr_tm.tm_hour, curr_tm.tm_min, curr_tm.tm_sec);
return timebuffer;
}
class Logger
{
public:
Logger()
{
// 默认使用控制台策略
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_flush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_flush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 一条日志信息
class LogMessage
{
public:
LogMessage(LogLevel& level, const std::string& src_name, int line, Logger& logger)
:_cur_time(GetTimeStamp())
,_level(level)
,_pid(getpid())
,_src_name(src_name)
,_line(line)
,_logger(logger)
{
// 日志的左边信息,合并起来
std::stringstream ss;
ss << "[" << _cur_time << "] "
<< "[" << LevelToStr(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str();
}
// 日志的右边信息,合并起来
template<class T>
LogMessage& operator<<(const T& info) // 注意使用引用返回,可以持续输入
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
// 如果刷新策略的指针不为空,就将日志刷新到选择的策略中
if(_logger._flush_strategy)
{
_logger._flush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _cur_time; // 当前时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _src_name; // 源文件名
int _line; // 行号
std::string _loginfo; // 一条完整日志信息
Logger& _logger;
};
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
~Logger() {}
private:
std::unique_ptr<LogStrategy> _flush_strategy;
};
// 定义全局对象
Logger logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()主要组成部分包括:
LevelToStr和GetTimeStamp
在GetTimeStamp()函数中使用了几个重要的时间相关系统调用:
std::string GetTimeStamp()
{
time_t cur = time(nullptr); // 1. 获取当前时间戳
struct tm curr_tm;
localtime_r(&cur, &curr_tm); // 2. 转换为本地时间结构
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900, curr_tm.tm_mon + 1, curr_tm.tm_mday,
curr_tm.tm_hour, curr_tm.tm_min, curr_tm.tm_sec); // 3. 格式化时间
return timebuffer;
}详细说明:
time(nullptr):
nullptr表示不需要将结果存储在额外的地方
time_t类型的时间戳
localtime_r(&cur, &curr_tm):
time_t表示的时间转换为本地时间的tm结构
localtime()的区别:localtime_r是线程安全版本,它将结果存储在用户提供的缓冲区中
tm结构字段:
tm_year: 从1900年开始的年数
tm_mon: 月份(0-11)
tm_mday: 月中的天数(1-31)
tm_hour: 小时(0-23)
tm_min: 分钟(0-59)
tm_sec: 秒(0-60,60用于闰秒)
snprintf:
sprintf更安全,因为它限制了最大写入字节数
将LogMessage设计为Logger的内部类有几个重要优势:
LogMessage完全依赖于Logger的存在和功能
Logger的私有成员_flush_strategy
<<操作符接口
LogMessage对象
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}logger(level, file, line) << "message" << value;
LogMessage对象在完整表达式结束时析构
LOG(level)调用,例如:LOG(LogLevel::INFO) << "Hello" << value;
logger(LogLevel::INFO, __FILE__, __LINE__) << "Hello" << value;
Logger::operator(),创建并返回一个临时LogMessage对象
LogMessage构造函数收集时间、级别、进程ID等元信息
operator<<连续调用,构建日志内容
LogMessage对象析构
#include "Log.hpp"
using namespace LogModule;
void fun()
{
int a = 10;
LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c' << a;
}
int main()
{
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::WARNING) << "hello world";
fun();
return 0;
}运行结果;
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./test
[2025-09-09 23:15:59] [DEBUG] [3580018] [Main.cc] [12] - hello world
[2025-09-09 23:15:59] [DEBUG] [3580018] [Main.cc] [13] - hello world
[2025-09-09 23:15:59] [DEBUG] [3580018] [Main.cc] [14] - hello world
[2025-09-09 23:15:59] [DEBUG] [3580018] [Main.cc] [16] - hello world
[2025-09-09 23:15:59] [DEBUG] [3580018] [Main.cc] [17] - hello world
[2025-09-09 23:15:59] [WARNING] [3580018] [Main.cc] [18] - hello world
[2025-09-09 23:15:59] [FATAL] [3580018] [Main.cc] [8] - hello world1234, 3.14c10