本文主要内容:
内容梳理:
本节内容,我们来学习一下日志系统。
什么是日志系统?
日志系统到底是干什么的?
为什么需要日志系统?
日志系统的一个工作原理是什么?
我比较疑惑的是,日志中的内容是是怎么来的?
- 是由机器检测,然后写入日志文件?
- 还是程序员事先编写好相关内容,当程序执行一些步骤的时候,就将事先编写好的内容写入到日志系统中?
最后写出日志系统的代码。(代码讲解在后文)(代码中我认为比较陌生的知识点都已经在番外链接说明了。)
然后在我们线程7.1中的线程池代码上使用。
番外链接:
- std_string_view-CSDN博客
- std_ofstream-CSDN博客
- C++ 枚举类(enum class)-CSDN博客
- C 风格时间获取方式详解-CSDN博客
- C++ 读写锁 std_shared_mutex-CSDN博客
- C++ std_stringstream-CSDN博客
- dynamic_cast 简单说明-CSDN博客
- 单例模式_-CSDN博客
- C++ 互斥锁-CSDN博客
- C++ stdatomic-CSDN博客
线程完整系列:
最终版日志系统代码
// Log.hpp
#ifndef LOG_HPP
#define LOG_HPP
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <vector>
#include <mutex>
#include <unistd.h>
#include <string_view>
#include <shared_mutex>
#include <atomic> // 用于 std::atomic
#include <cstdio> // 用于 snprintf
#include <ctime> // 用于 time_t, tm
#include <stdexcept> // 用于 std::runtime_error
#include <functional> // 用于 std::ref
// 需要添加这个头文件用于 dynamic_cast
#include <typeinfo>
// 日志输出目标接口
// (日志可以输出到终端上,也可以输出到某个文件上)
class LogOutputTarget
{
public:
virtual ~LogOutputTarget() = default;
virtual void write(std::string_view message) = 0; // 使用string_view
};
// 在终端/控制台打印日志信息 // 将日志内容输出到终端
class Console_OutputTarget : public LogOutputTarget
{
private:
static std::mutex &get_cout_mutex()
{
static std::mutex cout_mutex;
return cout_mutex;
}
public:
void write(std::string_view message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(get_cout_mutex());
std::cout << message << '\n'; // 避免std::endl的flush
}
};
// 将日志内容输出到文件中
class File_OutputTarget : public LogOutputTarget
{
private:
std::ofstream file;
std::string buffer;
static const size_t BUFFER_SIZE = 4096;
std::mutex file_mutex; // 使用独立的mutex (为什么不用静态锁呢?有讲究的,在deek)
std::atomic<bool> destroyed{false};
std::string filename;
public:
File_OutputTarget(const std::string &filename) : filename(filename)
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
throw std::runtime_error("Cannot open log file: " + filename);
}
}
~File_OutputTarget()
{
destroyed.store(true, std::memory_order_release);
flush();
}
void write(std::string_view message) override
{
// 先快速检查,避免不必要的锁竞争
if (destroyed.load(std::memory_order_acquire))
{
return;
}
std::lock_guard<std::mutex> lock(file_mutex);
// 双重检查,防止在等待锁的过程中被销毁
if (destroyed.load(std::memory_order_acquire))
{
return;
}
buffer.append(message);
buffer.push_back('\n');
if (buffer.size() >= BUFFER_SIZE)
{
flush_impl();
}
}
void flush()
{
std::lock_guard<std::mutex> lock(file_mutex);
flush_impl();
}
bool is_open() const
{
return file.is_open();
}
private:
// 为什么没有在这个函数开始执行的时候加锁?
// 因为所有调用这个函数的函数都已经加了锁,就好比如flush()函数,write函数
// 所以就不能再在这个函数中加锁,否则就会形成死锁
// 而且这个函数是私有的,类外是无法调用的,所以无需担心
void flush_impl()
{
if (!file.is_open() || !file.good()) // 同时检查文件状态
{
// 先清除可能的错误状态
file.clear();
// 尝试重新打开文件
try
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
std::cerr << "Failed to reopen log file: " << filename << std::endl;
return;
}
}
catch (const std::exception &e)
{
std::cerr << "Exception reopening log file: " << e.what() << std::endl;
return;
}
}
if (!buffer.empty())
{
try
{
file << buffer;
file.flush();
// 检查写入是否成功
if (!file.good())
{
throw std::runtime_error("File write failed or stream is in bad state");
}
buffer.clear();
}
catch (const std::exception &e)
{
std::cerr << "Log write failed: " << e.what() << std::endl;
// 清空缓冲区,避免重复写入失败的数据
buffer.clear();
}
}
}
};
// ------------------------------------------------------------------------------
// 日志常规信息:日志时间,日志级别
// 日志详细信息:日志时间,日志级别,文件名,行号,进程PID
// 日志级别信息
enum class LogLevel
{
TRACE, // 最详细的调试信息
DEBUG, // 调试信息
INFO, // 一般信息
WARN, // 警告信息
ERROR, // 错误信息
FATAL // 严重错误
};
// 将日志级别转换为字符串
// constexpr: 允许编译器计算, 可能完全内联,减少运行时的开销
// noexcept: 告诉编译器不会抛出异常,便于优化
constexpr const char *LevelToString(LogLevel level) noexcept
{
switch (level)
{
case LogLevel::TRACE:
return "[TRACE] ";
case LogLevel::DEBUG:
return "[DEBUG] ";
case LogLevel::INFO:
return "[INFO] ";
case LogLevel::WARN:
return "[WARN] ";
case LogLevel::ERROR:
return "[ERROR] ";
case LogLevel::FATAL:
return "[FATAL] ";
default:
return "[UNKNOWN] ";
}
}
// ------------------------------------------------------------------------------
// 时间格式化缓存优化
class TimeCache
{
private:
std::atomic<time_t> last_second{0};
std::string cached_time;
std::mutex time_mutex;
public:
std::string get_current_time()
{
time_t now = time(nullptr);
time_t last = last_second.load(std::memory_order_acquire);
if (now != last)
{
std::lock_guard<std::mutex> lock(time_mutex);
// 双重检查
if (now != last_second.load(std::memory_order_relaxed))
{
char buffer[64];
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(buffer, sizeof(buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900, time_info.tm_mon + 1,
time_info.tm_mday, time_info.tm_hour,
time_info.tm_min, time_info.tm_sec);
cached_time = buffer;
last_second.store(now, std::memory_order_release);
}
}
return cached_time;
}
};
// ------------------------------------------------------------------------------
// 主日志类(最外层类)
class Logger
{
private:
// 日志当前级别
std::atomic<LogLevel> currentLevel;
// 存储日志输出目标的数组
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
// 读写锁
std::shared_mutex targets_mutex;
// 缓存时间类
TimeCache time_cache;
private:
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
}
void output_to_targets(const std::string &message)
{
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex);
targets_copy.reserve(outputTargets.size());
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get());
}
}
for (auto target : targets_copy)
{
target->write(message);
}
}
public:
// 删除拷贝构造函数和赋值操作
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
// 虽然单例模式通常不需要,但为了完整性可以添加
Logger(Logger &&) = delete;
Logger &operator=(Logger &&) = delete;
static Logger *getInstance()
{
static Logger instance; // C++11 保证这是线程安全的
return &instance;
}
// 添加日志输出目标到数组中
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
// 先申请一把读写锁
std::unique_lock lock(targets_mutex);
outputTargets.push_back(std::move(outputtarget));
}
// 设置日志的级别
void setLevel(LogLevel level)
{
currentLevel.store(level, std::memory_order_release);
}
// 添加便捷的级别检查方法
bool shouldLog(LogLevel level) const
{
return level >= currentLevel.load(std::memory_order_acquire);
}
LogLevel getLevel() const
{
return currentLevel.load(std::memory_order_acquire);
}
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush();
}
// 也可以考虑刷新控制台输出
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
// 控制台输出通常是行缓冲的,但可以强制刷新
std::cout << std::flush;
}
}
}
// 基础情况:没有更多参数
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归处理参数
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos);
ss << value;
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
}
else
{
// 如果还有剩余参数,输出警告
ss << format;
if constexpr (sizeof...(args) >= 0) // 总是为 true,但确保编译
{
ss << " [EXTRA_ARGS: " << value;
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...);
}
ss << "]";
}
}
}
// 核心日志输出方法--简单信息版
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
// 将日志输出到目标文件
output_to_targets(ss.str());
}
// 核心日志输出方法--格式化信息版
template <typename... Args>
void log(std::string_view filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]"; // 日志发生地点
// 加入格式化日志内容
format_string_impl(std::ref(ss), format, std::forward<Args>(args)...);
// 将日志输出到目标文件
output_to_targets(ss.str());
}
};
// 方案1:统一宏
#define LOG(level, ...) \
do \
{ \
auto logger_instance = Logger::getInstance(); \
if (logger_instance) \
{ \
logger_instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__); \
} \
} while (0)
// 方案2:级别特定的宏
#define TRACE(...) LOG(LogLevel::TRACE, ##__VA_ARGS__)
#define DEBUG(...) LOG(LogLevel::DEBUG, ##__VA_ARGS__)
#define INFO(...) LOG(LogLevel::INFO, ##__VA_ARGS__)
#define WARN(...) LOG(LogLevel::WARN, ##__VA_ARGS__)
#define ERROR(...) LOG(LogLevel::ERROR, ##__VA_ARGS__)
#define FATAL(...) LOG(LogLevel::FATAL, ##__VA_ARGS__)
#endif // LOG_HPP
实践代码
我们可以通过下面的代码,简单验证一下我们的日志功能:
// Main.cc
#include <chrono>
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>
#include "Log.hpp"
int main()
{
// 获取单例并配置
Logger *logger = Logger::getInstance();
logger->setLevel(LogLevel::DEBUG);
logger->addOutputTarget(std::make_unique<Console_OutputTarget>());
logger->addOutputTarget(std::make_unique<File_OutputTarget>("app.log"));
ERROR("错误22222! {}", "aaaabbbb", 11111);
ERROR("错误22222! {} {}", "aaaabbbb", 11111);
ERROR("错误22222! {} {}", "aaaabbbb", 11111, "abcd777");
DEBUG("debug {}", "aaaabbbb", 11111);
INFO("info {}", "aaaabbbb", 11111);
ERROR("只有一个占位符 {}", "参数1", "参数2", "参数3");
ERROR("只有一个占位符 {}", "参数1", "参数2");
ERROR("没有占位符", "参数1", "参数2");
return 0;
}
运行结果:
lx@emperor:~/working/d47/threadtest/thread_pool$ ./threadpool
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:16]错误22222! aaaabbbb [EXTRA_ARGS: 11111]
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:17]错误22222! aaaabbbb 11111
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:18]错误22222! aaaabbbb 11111 [EXTRA_ARGS: abcd777]
[2025-11-24 14:02:50] [DEBUG] [60564] [Main.cc:20]debug aaaabbbb [EXTRA_ARGS: 11111]
[2025-11-24 14:02:50] [INFO] [60564] [Main.cc:21]info aaaabbbb [EXTRA_ARGS: 11111]
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:23]只有一个占位符 参数1 [EXTRA_ARGS: 参数2 参数3]
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:25]只有一个占位符 参数1 [EXTRA_ARGS: 参数2]
[2025-11-24 14:02:50] [ERROR] [60564] [Main.cc:26]没有占位符 [EXTRA_ARGS: 参数1 参数2]
将日志系统运用在线程池中
ThreadPool.hpp
// ThreadPool.hpp
#pragma once
#include <condition_variable>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <mutex>
#include <thread>
#include <queue>
#include <functional>
#include <atomic>
#include <type_traits>
#include "Task.hpp"
#include "Log.hpp"
// 默认线程数
const static int default_threadnum = 3;
template <typename T>
class ThreadPool
{
// 当要修改线程池中的线程个数的时候,一下元素需要跟随改变
// 第一:_num_thread 二:_work_threads
// 可能需要改变_num_wait
// _stop_flags的大小,以及其中线程对应的标志
private:
// 线程池中线程的个数
size_t _num_thread;
// 用vector来统一管理线程池中的线程
std::vector<std::thread> _work_threads;
// 任务队列(阻塞队列)
std::queue<T> _task_queue;
// 保护任务队列的互斥锁
std::mutex _task_queue_mutex;
// 条件变量:用于线程间通信
std::condition_variable _task_ready;
// 程序退出标志
std::atomic<bool> _stop;
// 正在等待中的消费者个数
std::atomic<int> _num_wait;
// 每个工作线程都有一个flag-->线程退出标志 false表示运行 true表示线程退出
std::vector<std::atomic<bool> *> _stop_flags;
public:
ThreadPool(int num_thread = default_threadnum)
: _num_thread(num_thread), _stop(false), _num_wait(0)
{
if (_num_thread == 0)
{
throw std::invalid_argument("线程数不能为0!!!");
}
try
{
// 先初始化标志数组
for (size_t i = 0; i < _num_thread; ++i)
{
_stop_flags.emplace_back(new std::atomic<bool>(false));
};
// 再创建线程(线程启动后立即可以安全访问_stop_flags)
for (size_t i = 0; i < _num_thread; ++i)
{
_work_threads.emplace_back([this, i]
{ worker_thread(i); });
}
INFO("已创建线程池, 线程池中有 {} 个消费者. ", _num_thread);
}
catch (const std::exception &e)
{
ERROR("线程池创建失败: {}", e.what());
// std::cerr << "线程池创建失败: " << e.what() << std::endl;
SoftExit(); // 清理已创建的资源
// 清理动态分配的内存
for (auto flag : _stop_flags)
{
delete flag;
}
throw; // 重新抛出异常
}
}
// 单独停止某一个线程
void stop_thread(size_t thread_id)
{
if (thread_id < _num_thread)
{
INFO("正在停止线程 {}", thread_id);
_stop_flags[thread_id]->store(true, std::memory_order_release);
_task_ready.notify_all(); // 唤醒所有线程检查停止标志
if (_work_threads[thread_id].joinable())
{
_work_threads[thread_id].join();
}
// 本函数只负责停止线程,不负责处理_work_threads
INFO("线程 {} 已经成功退出.", thread_id);
}
else
{
WARN("无效的线程停止操作: thread_id {} out of range", thread_id);
// std::cout << "操作不合法, 无法停止范围之外的线程! " << std::endl;
}
}
// 强制退出所有线程池
// 有过锁的问题,讨论是否要加锁,讨论加锁的粒度,加锁的位置问题
void ForceExit()
{
INFO("开始强制退出线程池. ");
// 第一步:设置停止标志并唤醒线程(短暂加锁)
{
std::lock_guard<std::mutex> lock(_task_queue_mutex);
if (_stop.exchange(true, std::memory_order_acq_rel))
{
return;
}
// 设置所有线程的停止标志
for (size_t i = 0; i < _stop_flags.size(); ++i)
{
_stop_flags[i]->store(true, std::memory_order_release);
}
_task_ready.notify_all();
} // 锁在这里释放
// 第二步:等待线程结束(不加锁)
for (auto &thread : _work_threads)
{
if (thread.joinable())
{
thread.join();
}
}
INFO("线程池已经成功强制退出! ");
}
void SoftExit()
{
INFO("线程池开始的平稳退出. ");
// 第一步:设置停止标志并唤醒线程(短暂加锁)
{
std::lock_guard<std::mutex> lock(_task_queue_mutex);
if (_stop.exchange(true, std::memory_order_acq_rel))
{
return;
}
// 设置所有线程的停止标志
// for (size_t i = 0; i < _stop_flags.size(); ++i)
// {
// _stop_flags[i]->store(true, std::memory_order_release);
// }
_task_ready.notify_all();
} // 锁在这里释放
// 第二步:等待线程结束(不加锁)
for (auto &thread : _work_threads)
{
if (thread.joinable())
{
thread.join();
}
}
INFO("线程池平稳的退出成功. ");
}
void ResizePool(size_t new_size)
{
INFO("Resizing thread pool from {} to {}", _work_threads.size(), new_size);
// 用于记录需要停止的线程ID
std::vector<size_t> threads_to_stop;
{
std::lock_guard<std::mutex> lock(_task_queue_mutex);
if (_stop.load())
return;
if (new_size > _work_threads.size())
{
// 增加线程
size_t old_size = _work_threads.size();
for (size_t i = old_size; i < new_size; i++)
{
_stop_flags.emplace_back(new std::atomic<bool>(false));
_work_threads.emplace_back([this, i]
{ worker_thread(i); });
}
_num_thread = new_size;
INFO("增加了{} 个线程, 现在消费者个数为{} .", new_size - old_size, _num_thread);
return;
}
else if (new_size < _work_threads.size())
{
// 减少线程
size_t to_reduce = _work_threads.size() - new_size;
for (size_t i = 0; i < to_reduce; i++)
{
threads_to_stop.push_back(_work_threads.size() - 1 - i);
}
}
}
// 按从大到小排序,避免删除时索引变化
std::sort(threads_to_stop.rbegin(), threads_to_stop.rend());
// 曾经有死锁问题,要在锁外等待线程
// 在锁外停止线程
for (size_t thread_id : threads_to_stop)
{
// 停止线程(这个函数内部会等待线程退出)
stop_thread(thread_id);
// 清理资源(需要短暂获取锁)
{
std::lock_guard<std::mutex> lock(_task_queue_mutex);
// 注意:删除后索引会变化,但我们是按从大到小处理的,所以安全
// 删除对应的停止标志
delete _stop_flags[thread_id];
_stop_flags.erase(_stop_flags.begin() + thread_id);
_work_threads.erase(_work_threads.begin() + thread_id);
_num_thread--;
}
}
INFO("减少了{} 个线程, 现在消费者个数为{} .", threads_to_stop.size(), _num_thread);
}
// 添加任务到队列
bool enqueue(const T &task)
{
// 先申请锁
std::lock_guard<std::mutex> lock(_task_queue_mutex);
// 先申请锁主要是为了避免竞态条件的出现
// 先判断程序是否该结束
if (_stop.load())
{
WARN("线程池已经结束, 无法继续添加任务 .");
// std::cout << "线程池已经结束, 无法继续添加任务 " << std::endl;
return false;
}
// 申请到锁后,添加任务到队列中
_task_queue.emplace(task);
DEBUG("任务已入队, 当前任务队列大小: {}, 正在等待任务的人员数量: {}",
_task_queue.size(), _num_wait.load());
// 如果此时有消费者正在等待,则唤醒其中一个消费者
if (_num_wait.load() > 0)
{
_task_ready.notify_one();
INFO("唤醒一名等待者处理任务! ");
}
return true;
}
// 添加任务到队列
bool enqueue(T &&task)
{
// 先申请锁
std::lock_guard<std::mutex> lock(_task_queue_mutex);
// 接着我们判断程序是否结束, 如果结束了就不能继续添加任务
if (_stop.load())
{
WARN("线程池已经结束, 无法继续添加任务 .");
// std::cout << "线程池已经结束, 无法继续添加任务 " << std::endl;
return false;
}
// 申请到锁后,添加任务到队列中
_task_queue.emplace(std::move(task));
DEBUG("任务已入队, 当前任务队列大小: {}, 正在等待任务的人员数量: {}",
_task_queue.size(), _num_wait.load());
// 此处大家可以打印出一些信息,观察代码运行情况
// std::cout << "正在等待的消费者: " << _num_wait.load()
// << "活跃的消费者:" << 4 - _num_wait.load()
// << "当前队列中的任务量:" << _task_queue.size() << std::endl;
// 如果此时有消费者正在等待,则唤醒其中一个消费者
if (_num_wait.load() > 0)
{
_task_ready.notify_one();
INFO("唤醒一名等待者处理任务! ");
}
return true;
}
// 获取任务队列大小(线程安全)
size_t queue_size()
{
// 先申请锁
// 因为访问的是任务队列,为了防止读取队列信息过程中有人
// 修改队列,所以我们要加锁保护
std::lock_guard<std::mutex> lock(_task_queue_mutex);
return _task_queue.size();
}
// 获取 线程池 中 工作线程 的个数
size_t num_Of_worker()
{
return _work_threads.size();
}
// 判断线程池是否停止
bool is_stopped() const
{
return _stop.load();
}
// 判断线程池 的 任务队列 是否为空
bool is_empty()
{
return queue_size() == 0;
}
// 禁止拷贝和赋值
ThreadPool(const ThreadPool &) = delete;
ThreadPool &operator=(const ThreadPool &) = delete;
// 析构函数
~ThreadPool()
{
SoftExit();
// 清理动态分配的内存
for (auto flag : _stop_flags)
{
delete flag;
}
}
private:
// 工作线程函数
void worker_thread(size_t thread_id)
{
std::string thread_name = "worker-" + std::to_string(thread_id);
INFO("线程 {} 启动", thread_name);
while (true)
{
T task;
{
// 先申请锁, 保证队列安全
std::unique_lock<std::mutex> lock(_task_queue_mutex);
// 先检查线程特定的停止标志
if (_stop_flags[thread_id]->load(std::memory_order_acquire))
{
INFO("线程 {} 收到单独停止信号 .", thread_name);
// std::cout << thread_name << "收到单独停止信号" << std::endl;
break;
}
// 如果队列中是空的
if (_task_queue.empty())
{
// 且程序此时需要退出
if (_stop.load(std::memory_order_acquire))
{
// 则退出循环, 工作线程准备退出
break;
}
else
{
// 程序此时还没退出,则进入等待
// 如果程序设置了退出标志(即需要退出的时候) 或者 队列不为空, 则可以返回继续执行
// 等待的工作线程数量++
++_num_wait;
DEBUG("线程 {} 正在等待任务, 当前等待人员数量: {}",
thread_name, _num_wait.load());
_task_ready.wait(lock, [this, thread_id]
{ return _stop.load(std::memory_order_acquire) ||
_stop_flags[thread_id]->load(std::memory_order_acquire) ||
!_task_queue.empty(); });
// 返回后等待的工作线程数量--
--_num_wait;
// 因为线程返回的时候,可能是因为程序需要退出了,才被唤醒返回的
// 或者线程被指定退出了
// 所有我们要增加一次对程序退出标志的判断
if (_stop.load(std::memory_order_acquire) ||
_stop_flags[thread_id]->load(std::memory_order_acquire))
{
break;
}
}
}
else // 如果队列不为空
{
// 则获取队列中的第一个任务
if constexpr (std::is_same_v<T, Task>)
{
task = _task_queue.front();
}
else if constexpr (std::is_same_v<T, std::unique_ptr<Task>>)
{
task = std::move(_task_queue.front());
}
// 获取完队列中的第一个任务后,就要将这个任务删除
_task_queue.pop();
// 拿完数据后就直接解锁
lock.unlock();
// 现在在锁外执行任务
try
{
DEBUG("线程 {} 正在处理任务", thread_name);
if constexpr (std::is_same_v<T, Task>)
{
task();
INFO("线程 {}: 任务结果为: {}", thread_name, task.ResultToString());
// std::cout << thread_name << ": " << task.ResultToString() << std::endl;
}
else if constexpr (std::is_same_v<T, std::unique_ptr<Task>>)
{
if (task)
{
task->Excute();
INFO("线程 {}: 任务结果为: {}", thread_name, task->ResultToString());
// std::cout << thread_name << ": " << task->ResultToString() << std::endl;
}
}
}
catch (const std::exception &e)
{
ERROR("Exception in {}: {}", thread_name, e.what());
// std::cerr << "Exception in " << thread_name << ": " << e.what() << std::endl;
}
// 继续循环,不需要额外的break
continue;
}
}
}
INFO("线程 {} exited", thread_name);
// std::cout << "Thread " << thread_name << " exited" << std::endl;
}
};
Main.cc
// Main.cc
#include <chrono>
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>
#include "Log.hpp"
int main()
{
// 获取单例并配置
Logger *logger = Logger::getInstance();
logger->setLevel(LogLevel::DEBUG);
logger->addOutputTarget(std::make_unique<Console_OutputTarget>());
logger->addOutputTarget(std::make_unique<File_OutputTarget>("app.log"));
// 创建线程池
ThreadPool<std::unique_ptr<Task>> pool(4);
srand(time(nullptr) ^ getpid() ^ pthread_self());
// 添加任务
for (int i = 0; i < 44; ++i)
{
int a = rand() % 10 + 1;
int b = rand() % 5 + 1;
std::unique_ptr<Task> task = std::make_unique<Task>(a, b);
INFO("第 {} 个任务为: {} ", i, task->DebugToString());
// std::cout << task->DebugToString() << std::endl;
pool.enqueue(std::move(task));
if (i == 22)
{
pool.ResizePool(7);
}
// 模拟生产间隔
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// 等待队列中的任务被清空
while (!pool.is_empty())
{
std::cout << "剩余任务: " << pool.queue_size() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
pool.ResizePool(4);
// 停止线程池
pool.SoftExit();
logger->flush_all();
return 0;
}
运行结果:(运行结果取决于每次的线程调度顺序)
lx@emperor:~/working/d47/threadtest/thread_pool$ ./threadpool
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-0 启动
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:72]已创建线程池, 线程池中有 4 个消费者.
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 1
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 0 个任务为: 4+5=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 1
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-1 启动
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 4+5=9
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 2
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-2 启动
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 3
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-3 启动
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 1 个任务为: 3+1=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 3+1=4
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 2 个任务为: 7+1=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 7+1=8
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 3 个任务为: 1+2=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 1+2=3
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 4 个任务为: 8+4=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 8+4=12
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 5 个任务为: 3+3=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 3+3=6
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 6 个任务为: 7+3=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 7+3=10
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 7 个任务为: 2+3=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 2+3=5
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 8 个任务为: 8+4=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 8+4=12
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 9 个任务为: 7+5=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 7+5=12
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 10 个任务为: 1+2=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 1+2=3
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 11 个任务为: 5+1=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 5+1=6
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 12 个任务为: 2+1=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 2+1=3
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 13 个任务为: 7+2=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 7+2=9
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 14 个任务为: 3+3=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 3+3=6
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 15 个任务为: 1+1=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 1+1=2
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 16 个任务为: 7+5=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 7+5=12
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 17 个任务为: 1+4=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 1+4=5
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 18 个任务为: 10+3=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 10+3=13
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [Main.cc:28]第 19 个任务为: 7+5=?
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:57] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 7+5=12
[2025-11-24 14:06:57] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 20 个任务为: 1+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 1+1=2
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 21 个任务为: 2+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 2+2=4
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 22 个任务为: 8+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 4
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:181]Resizing thread pool from 4 to 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:204]增加了3 个线程, 现在消费者个数为7 .
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 8+1=9
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-5 启动
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 4
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-5 正在等待任务, 当前等待人员数量: 5
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-4 启动
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-4 正在等待任务, 当前等待人员数量: 6
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:351]线程 worker-6 启动
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-6 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 23 个任务为: 2+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 2+2=4
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 24 个任务为: 4+3=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 4+3=7
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 25 个任务为: 3+4=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 3+4=7
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 26 个任务为: 6+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 6+2=8
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 27 个任务为: 4+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-5 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-5: 任务结果为: 4+2=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-5 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 28 个任务为: 4+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-4 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-4: 任务结果为: 4+2=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-4 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 29 个任务为: 5+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-6 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-6: 任务结果为: 5+1=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-6 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 30 个任务为: 4+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 4+2=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 31 个任务为: 1+5=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 1+5=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 32 个任务为: 2+3=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 2+3=5
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 33 个任务为: 5+3=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 5+3=8
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 34 个任务为: 5+3=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-5 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-5: 任务结果为: 5+3=8
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-5 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 35 个任务为: 2+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-4 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-4: 任务结果为: 2+1=3
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-4 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 36 个任务为: 3+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-6 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-6: 任务结果为: 3+1=4
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-6 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 37 个任务为: 4+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-3 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-3: 任务结果为: 4+2=6
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-3 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 38 个任务为: 1+2=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-0 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-0: 任务结果为: 1+2=3
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-0 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [Main.cc:28]第 39 个任务为: 10+1=?
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-1 正在处理任务
[2025-11-24 14:06:58] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-1: 任务结果为: 10+1=11
[2025-11-24 14:06:58] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-1 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [Main.cc:28]第 40 个任务为: 4+4=?
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-2 正在处理任务
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-2: 任务结果为: 4+4=8
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-2 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [Main.cc:28]第 41 个任务为: 9+1=?
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-5 正在处理任务
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-5: 任务结果为: 9+1=10
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-5 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [Main.cc:28]第 42 个任务为: 3+4=?
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-4 正在处理任务
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-4: 任务结果为: 3+4=7
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-4 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [Main.cc:28]第 43 个任务为: 10+1=?
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:285]任务已入队, 当前任务队列大小: 1, 正在等待任务的人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:297]唤醒一名等待者处理任务!
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:423]线程 worker-6 正在处理任务
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:435]线程 worker-6: 任务结果为: 10+1=11
[2025-11-24 14:06:59] [DEBUG] [60598] [ThreadPool.hpp:382]线程 worker-6 正在等待任务, 当前等待人员数量: 7
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:181]Resizing thread pool from 7 to 4
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:93]正在停止线程 6
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-6 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:102]线程 6 已经成功退出.
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:93]正在停止线程 5
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-5 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:102]线程 5 已经成功退出.
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:93]正在停止线程 4
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-4 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:102]线程 4 已经成功退出.
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:240]减少了3 个线程, 现在消费者个数为4 .
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:148]线程池开始的平稳退出.
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-1 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-2 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-3 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:452]线程 worker-0 exited
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:176]线程池平稳的退出成功.
[2025-11-24 14:06:59] [INFO] [60598] [ThreadPool.hpp:148]线程池开始的平稳退出.
日志系统:从原理到实践
原理省流
首先我们先大概了解一下日志大概是什么模样,我们使用一个类比来给大家解释一下:
日志其实就类似现实生活中,我们做数学题的时候,解题步骤中的关键步骤处的文字说明。这个文字说明有以下这些作用:
- 解释下面的解题逻辑,告诉老师,你为什么要这样解题。
- 当解题错误后,你可以根据这些文字说明,梳理解题逻辑,找寻错误原因,发现逻辑错误。
日志大概就是这样一个东西。
回到程序中,日志其实就是一个更加全面的,智能的cout。平时写程序的时候,不是会在一些关键步骤使用cout打印关键步骤的信息和内容吗?
日志就是比cout更高级一点,更智能一些的cout。也是给程序员用来打印关键步骤的信息和内容用的。
什么是日志系统?
日志系统是一个系统化的记录机制,用于捕获、存储和管理软件运行时产生的各种事件信息。它就像程序的"黑匣子",记录着系统运行过程中的关键活动、状态变化、错误信息等。
日志系统到底是干什么的?
日志系统主要承担以下几个核心功能:
1. 问题诊断与调试
// 示例:在关键业务逻辑处添加日志
void processOrder(Order& order) {
logger.info("开始处理订单,订单ID: {}", order.getId());
try {
validateOrder(order);
logger.debug("订单验证通过");
processPayment(order);
logger.info("支付处理完成,金额: {}", order.getAmount());
} catch (const PaymentException& e) {
logger.error("支付处理失败,订单ID: {}, 错误: {}",
order.getId(), e.what());
throw;
}
}
2. 系统监控与性能分析
// 示例:性能监控日志
class PerformanceMonitor {
void processRequest(Request& req) {
auto start = std::chrono::steady_clock::now();
// 业务处理...
handleBusinessLogic(req);
auto duration = std::chrono::steady_clock::now() - start;
logger.info("请求处理完成,耗时: {}ms",
std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
}
};
3. 安全审计与合规性
// 示例:安全审计日志
class UserService {
void login(const std::string& username, const std::string& ip) {
logger.audit("用户登录: 用户名={}, IP地址={}, 时间={}",
username, ip, getCurrentTime());
if (checkSuspiciousActivity(username, ip)) {
logger.security("检测到可疑登录行为: {}", username);
}
}
};
为什么需要日志系统?
1. 替代原始的调试方式
没有日志系统时的调试:
// ❌ 不好的做法:使用cout调试
void complexFunction() {
std::cout << "进入函数complexFunction" << std::endl; // 调试信息
// ... 一些逻辑
std::cout << "执行到第25行" << std::endl;
if (someCondition) {
std::cout << "条件满足,执行分支A" << std::endl;
} else {
std::cout << "条件不满足,执行分支B" << std::endl;
}
std::cout << "函数执行完成" << std::endl;
}
问题:
- 调试信息与正常输出混杂
- 无法控制输出级别(开发/生产环境)
- 缺乏时间戳、上下文信息
- 性能影响(同步IO)
2. 生产环境的问题排查
在生产环境中:
- 无法使用调试器
- 需要重现问题场景
- 需要完整的执行上下文
- 需要长期的问题追踪
日志系统的工作原理
核心组件架构
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 应用程序代码 │───▶│ 日志记录器 │───▶│ 日志输出目标 │
│ │ │ (Logger) │ │ (Appender) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
生成日志消息 格式化日志 输出到文件/
设置日志级别 控制台/网络等
详细工作流程
1. 日志生成(核心疑问)
日志内容确实是由程序员预先编写的,但比简单的cout要智能得多:
// 传统cout方式(不推荐)
void processData(const std::vector<int>& data) {
std::cout << "开始处理数据,数据大小: " << data.size() << std::endl;
for (int i = 0; i < data.size(); ++i) {
std::cout << "处理第" << i << "个元素: " << data[i] << std::endl;
// 业务逻辑...
}
std::cout << "数据处理完成" << std::endl;
}
// 使用日志系统的方式(推荐)
class DataProcessor {
private:
Logger& logger; // 日志记录器实例
public:
void processData(const std::vector<int>& data) {
// 程序员预先编写日志语句
logger.info("开始处理数据,数据大小: {}", data.size());
try {
for (size_t i = 0; i < data.size(); ++i) {
// 调试级别的日志,在生产环境可能不输出
logger.debug("处理第{}个元素: {}", i, data[i]);
// 业务逻辑
processSingleItem(data[i]);
}
logger.info("数据处理成功完成");
} catch (const std::exception& e) {
// 错误日志,包含异常信息
logger.error("数据处理失败: {}", e.what());
throw;
}
}
};
2. 日志分级与过滤
日志系统支持不同的级别,可以根据环境灵活控制:
// 日志级别示例
enum class LogLevel {
TRACE, // 最详细的调试信息
DEBUG, // 调试信息
INFO, // 一般信息
WARN, // 警告信息
ERROR, // 错误信息
FATAL // 严重错误
};
// 不同环境使用不同级别
class LogConfig {
public:
// 开发环境:显示所有日志
static LogLevel getDevLevel() { return LogLevel::DEBUG; }
// 生产环境:只显示重要日志
static LogLevel getProdLevel() { return LogLevel::INFO; }
};
3. 日志格式化
// 日志格式化示例
class LogFormatter {
public:
std::string format(LogLevel level, const std::string& message) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
ss << " [" << levelToString(level) << "] ";
ss << message;
return ss.str();
}
};
4. 日志输出
// 多种输出方式
class LogAppender {
public:
virtual void write(const std::string& message) = 0;
};
class FileAppender : public LogAppender {
std::ofstream file;
public:
void write(const std::string& message) override {
file << message << std::endl;
}
};
class ConsoleAppender : public LogAppender {
public:
void write(const std::string& message) override {
std::cout << message << std::endl;
}
};
class NetworkAppender : public LogAppender {
public:
void write(const std::string& message) override {
// 发送到日志服务器
sendToLogServer(message);
}
};
日志内容的来源:详细解释
疑问解答
“日志中的内容是怎么来的?是由机器检测,然后写入日志文件,还是程序员事先编写好相关内容?”
答案是:两者都有,但主要是程序员预先编写。
1. 程序员显式编写(主要方式)
程序员在编写代码时,在关键的逻辑点、错误处理点、状态变化点等位置预先插入日志语句:
class DatabaseService {
private:
Logger& logger;
public:
bool connect(const std::string& connectionString) {
// 程序员预先编写:连接开始日志
logger.info("尝试连接数据库: {}", connectionString);
try {
auto start = std::chrono::steady_clock::now();
bool success = realConnect(connectionString);
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
if (success) {
// 程序员预先编写:连接成功日志
logger.info("数据库连接成功,耗时{}ms", duration.count());
} else {
// 程序员预先编写:连接失败日志
logger.warn("数据库连接失败,耗时{}ms", duration.count());
}
return success;
} catch (const std::exception& e) {
// 程序员预先编写:异常日志
logger.error("数据库连接异常: {}", e.what());
return false;
}
}
};
2. 系统/框架自动生成
一些成熟的框架或系统会自动生成部分日志:
// Web框架可能自动记录请求日志
class WebFramework {
public:
void handleRequest(const HttpRequest& req, HttpResponse& res) {
// 框架自动记录(程序员配置格式,但内容自动生成)
logger.info("HTTP请求: 方法={}, 路径={}, IP={}",
req.getMethod(), req.getPath(), req.getClientIP());
// 处理请求...
processRequest(req, res);
// 框架自动记录响应日志
logger.info("HTTP响应: 状态码={}, 大小={}字节",
res.getStatusCode(), res.getContentSize());
}
};
3. 监控系统自动检测
在一些复杂的系统中,监控组件会自动检测并记录:
class SystemMonitor {
private:
Logger& logger;
std::chrono::steady_clock::time_point lastCheck;
public:
void checkSystemHealth() {
auto now = std::chrono::steady_clock::now();
if (now - lastCheck > std::chrono::minutes(1)) {
// 自动检测系统状态并记录
double cpuUsage = getCpuUsage();
size_t memoryUsage = getMemoryUsage();
if (cpuUsage > 90.0) {
logger.warn("CPU使用率过高: {}%", cpuUsage);
}
if (memoryUsage > 1024 * 1024 * 1024) { // 1GB
logger.warn("内存使用过高: {}MB", memoryUsage / 1024 / 1024);
}
lastCheck = now;
}
}
};
日志系统 vs cout调试:核心区别
简单的cout调试
// ❌ 传统调试方式
void businessLogic() {
std::cout << "=== 开始执行业务逻辑 ===" << std::endl;
int result = calculate();
std::cout << "计算结果: " << result << std::endl;
if (result > 100) {
std::cout << "结果大于100,执行特殊处理" << std::endl;
specialProcess();
} else {
std::cout << "结果正常,继续处理" << std::endl;
normalProcess();
}
std::cout << "业务逻辑执行完成" << std::endl;
}
专业的日志系统
// ✅ 专业日志方式
void businessLogic() {
logger.info("开始执行业务逻辑");
int result = calculate();
logger.debug("计算结果: {}", result);
if (result > 100) {
logger.warn("计算结果异常,值: {},执行特殊处理", result);
specialProcess();
} else {
logger.debug("结果正常,继续标准处理");
normalProcess();
}
logger.info("业务逻辑执行完成");
}
核心区别对比
| 特性 | cout调试 | 日志系统 |
|---|---|---|
| 级别控制 | 无,所有信息都输出 | 支持TRACE/DEBUG/INFO/WARN/ERROR等级别 |
| 输出控制 | 需要手动注释/取消注释代码 | 配置文件控制,无需修改代码 |
| 格式化 | 简单的字符串拼接 | 结构化格式,包含时间戳、级别、文件行号等 |
| 输出目标 | 只能输出到控制台 | 文件、网络、数据库、控制台等 |
| 性能 | 同步IO,影响性能 | 支持异步写入,性能优化 |
| 生产环境 | 需要删除调试代码 | 可配置级别,生产环境只记录重要信息 |
| 搜索分析 | 困难,非结构化 | 结构化,便于搜索和分析 |
总结
日志系统的工作原理:
- 程序员预先编写日志语句在代码的关键位置
- 程序运行时,根据配置的日志级别决定是否输出
- 日志系统自动添加上下文信息(时间戳、级别等)
- 按照配置的格式和输出目标进行记录
与cout调试的本质区别:
- 日志系统是结构化、可配置、可管理的调试工具
- cout是临时、不可控、影响性能的原始调试方式
- 日志系统适用于开发、测试、生产全生命周期
- cout调试主要适用于临时性的开发阶段调试
日志系统让程序的"自言自语"变得有意义、可管理、可分析,是现代软件开发中不可或缺的基础设施。
初版代码
// Log.hpp
// #ifndef LOG_HPP
// #define LOG_HPP
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <vector>
#include <mutex>
#include <unistd.h>
// 日志输出目标接口
// (日志可以输出到终端上,也可以输出到某个文件上)
class LogOutputTarget
{
protected:
static std::mutex cout_mutex;
public:
virtual ~LogOutputTarget() = default;
virtual void write(const std::string &message) = 0;
};
// 静态成员变量必须在类外定义
std::mutex LogOutputTarget::cout_mutex;
// 在终端/控制台打印日志信息 // 将日志内容输出到终端
class Console_OutputTarget : public LogOutputTarget
{
public:
void write(const std::string &message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << message << std::endl;
}
};
// 将日志内容输出到文件中
class File_OutputTarget : public LogOutputTarget
{
private:
// std::ofstream 是 C++ 标准库中用于文件输出的类,
// 它继承自 std::ostream,专门用于将数据写入文件。
std::ofstream file;
public:
// 构造函数
File_OutputTarget(const std::string &filename)
{
// 以追加模式(在文件末尾写入)打开名为:filename的文件
file.open(filename, std::ios::app);
}
void write(const std::string &message) override
{
// 先申请锁
std::lock_guard<std::mutex> lock(cout_mutex);
// 先判断文件是否已经打开
if (file.is_open())
{
// 打开后,向文件写入日志内容
file << message << std::endl;
}
}
};
// ------------------------------------------------------------------------------
// 日志常规信息:日志时间,日志级别
// 日志详细信息:日志时间,日志级别,文件名,行号,进程PID
// 日志级别信息
enum class LogLevel
{
TRACE, // 最详细的调试信息
DEBUG, // 调试信息
INFO, // 一般信息
WARN, // 警告信息
ERROR, // 错误信息
FATAL // 严重错误
};
// 将日志级别转换为字符串
// constexpr: 允许编译器计算, 可能完全内联
// noexcept: 告诉编译器不会抛出异常,便于优化
constexpr const char *LevelToString(LogLevel level) noexcept
{
switch (level)
{
case LogLevel::TRACE:
return "[TRACE] ";
case LogLevel::DEBUG:
return "[DEBUG] ";
case LogLevel::INFO:
return "[INFO] ";
case LogLevel::WARN:
return "[WARN] ";
case LogLevel::ERROR:
return "[ERROR] ";
case LogLevel::FATAL:
return "[FATAL] ";
default:
return "[UNKNOWN] ";
}
}
// ------------------------------------------------------------------------------
// 主日志类(最外层类)
class Logger
{
private:
// 日志当前级别
LogLevel currentLevel;
// 存储日志输出目标的数组
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
public:
// 静态实例指针,用于宏访问
static Logger *instance;
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
instance = this;
}
// 添加日志输出目标到数组中
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
outputTargets.push_back(std::move(outputtarget));
}
// 设置日志的级别
void setLevel(LogLevel level)
{
currentLevel = level;
}
// 基础情况:没有更多参数
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归处理参数
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos); // 输出 {} 之前的部分
ss << value; // 输出参数值
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...); // 处理剩余部分
}
else
{
ss << format; // 没有找到 {},直接输出整个字符串
}
}
// 核心日志输出方法--简单信息版
void log(std::string filename, int line_num, LogLevel level, const char *message)
{
if (level < currentLevel)
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 用来存储时间信息
char time_buffer[64];
// 获取时间3件套
time_t now = time(nullptr);
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(time_buffer, sizeof(time_buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900,
time_info.tm_mon + 1,
time_info.tm_mday,
time_info.tm_hour,
time_info.tm_min,
time_info.tm_sec);
// 开始格式化日志内容
std::stringstream ss;
ss << time_buffer // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str());
}
}
// 核心日志输出方法--格式化信息版
template <typename... Args>
void log(std::string filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (level < currentLevel)
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 用来存储时间信息
char time_buffer[64];
// 获取时间3件套
time_t now = time(nullptr);
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(time_buffer, sizeof(time_buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900,
time_info.tm_mon + 1,
time_info.tm_mday,
time_info.tm_hour,
time_info.tm_min,
time_info.tm_sec);
// 开始格式化日志内容
std::stringstream ss;
ss << time_buffer // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]"; // 日志发生地点
// 加入日志内容
format_string_impl(std::ref(ss), format, std::forward<Args>(args)...);
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str());
}
}
};
// 静态成员定义
Logger *Logger::instance = nullptr;
// 方案1:统一宏(推荐)
#define LOG(level, ...) \
do \
{ \
if (Logger::instance) \
{ \
/* 使用函数重载而不是宏重载 */ \
Logger::instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__); \
} \
} while (0)
// 方案2:级别特定的宏
#define TRACE(...) LOG(LogLevel::TRACE, ##__VA_ARGS__)
#define DEBUG(...) LOG(LogLevel::DEBUG, ##__VA_ARGS__)
#define INFO(...) LOG(LogLevel::INFO, ##__VA_ARGS__)
#define WARN(...) LOG(LogLevel::WARN, ##__VA_ARGS__)
#define ERROR(...) LOG(LogLevel::ERROR, ##__VA_ARGS__)
#define FATAL(...) LOG(LogLevel::FATAL, ##__VA_ARGS__)
最终版代码讲解
// Log.hpp
#ifndef LOG_HPP
#define LOG_HPP
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <vector>
#include <mutex>
#include <unistd.h>
#include <string_view>
#include <shared_mutex>
#include <atomic> // 用于 std::atomic
#include <cstdio> // 用于 snprintf
#include <ctime> // 用于 time_t, tm
#include <stdexcept> // 用于 std::runtime_error
#include <functional> // 用于 std::ref
// 需要添加这个头文件用于 dynamic_cast
#include <typeinfo>
// 日志输出目标接口
// (日志可以输出到终端上,也可以输出到某个文件上)
class LogOutputTarget
{
public:
virtual ~LogOutputTarget() = default;
virtual void write(std::string_view message) = 0; // 使用string_view
};
// 在终端/控制台打印日志信息 // 将日志内容输出到终端
class Console_OutputTarget : public LogOutputTarget
{
private:
static std::mutex &get_cout_mutex()
{
static std::mutex cout_mutex;
return cout_mutex;
}
public:
void write(std::string_view message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(get_cout_mutex());
std::cout << message << '\n'; // 避免std::endl的flush
}
};
// 将日志内容输出到文件中
class File_OutputTarget : public LogOutputTarget
{
private:
std::ofstream file;
std::string buffer;
static const size_t BUFFER_SIZE = 4096;
std::mutex file_mutex; // 使用独立的mutex (为什么不用静态锁呢?有讲究的,在deek)
std::atomic<bool> destroyed{false};
std::string filename;
public:
File_OutputTarget(const std::string &filename) : filename(filename)
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
throw std::runtime_error("Cannot open log file: " + filename);
}
}
~File_OutputTarget()
{
destroyed.store(true, std::memory_order_release);
flush();
}
void write(std::string_view message) override
{
// 先快速检查,避免不必要的锁竞争
if (destroyed.load(std::memory_order_acquire))
{
return;
}
std::lock_guard<std::mutex> lock(file_mutex);
// 双重检查,防止在等待锁的过程中被销毁
if (destroyed.load(std::memory_order_acquire))
{
return;
}
buffer.append(message);
buffer.push_back('\n');
if (buffer.size() >= BUFFER_SIZE)
{
flush_impl();
}
}
void flush()
{
std::lock_guard<std::mutex> lock(file_mutex);
flush_impl();
}
bool is_open() const
{
return file.is_open();
}
private:
// 为什么没有在这个函数开始执行的时候加锁?
// 因为所有调用这个函数的函数都已经加了锁,就好比如flush()函数,write函数
// 所以就不能再在这个函数中加锁,否则就会形成死锁
// 而且这个函数是私有的,类外是无法调用的,所以无需担心
void flush_impl()
{
if (!file.is_open() || !file.good()) // 同时检查文件状态
{
// 先清除可能的错误状态
file.clear();
// 尝试重新打开文件
try
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
std::cerr << "Failed to reopen log file: " << filename << std::endl;
return;
}
}
catch (const std::exception &e)
{
std::cerr << "Exception reopening log file: " << e.what() << std::endl;
return;
}
}
if (!buffer.empty())
{
try
{
file << buffer;
file.flush();
// 检查写入是否成功
if (!file.good())
{
throw std::runtime_error("File write failed or stream is in bad state");
}
buffer.clear();
}
catch (const std::exception &e)
{
std::cerr << "Log write failed: " << e.what() << std::endl;
// 清空缓冲区,避免重复写入失败的数据
buffer.clear();
}
}
}
};
// ------------------------------------------------------------------------------
// 日志常规信息:日志时间,日志级别
// 日志详细信息:日志时间,日志级别,文件名,行号,进程PID
// 日志级别信息
enum class LogLevel
{
TRACE, // 最详细的调试信息
DEBUG, // 调试信息
INFO, // 一般信息
WARN, // 警告信息
ERROR, // 错误信息
FATAL // 严重错误
};
// 将日志级别转换为字符串
// constexpr: 允许编译器计算, 可能完全内联,减少运行时的开销
// noexcept: 告诉编译器不会抛出异常,便于优化
constexpr const char *LevelToString(LogLevel level) noexcept
{
switch (level)
{
case LogLevel::TRACE:
return "[TRACE] ";
case LogLevel::DEBUG:
return "[DEBUG] ";
case LogLevel::INFO:
return "[INFO] ";
case LogLevel::WARN:
return "[WARN] ";
case LogLevel::ERROR:
return "[ERROR] ";
case LogLevel::FATAL:
return "[FATAL] ";
default:
return "[UNKNOWN] ";
}
}
// ------------------------------------------------------------------------------
// 时间格式化缓存优化
class TimeCache
{
private:
std::atomic<time_t> last_second{0};
std::string cached_time;
std::mutex time_mutex;
public:
std::string get_current_time()
{
time_t now = time(nullptr);
time_t last = last_second.load(std::memory_order_acquire);
if (now != last)
{
std::lock_guard<std::mutex> lock(time_mutex);
// 双重检查
if (now != last_second.load(std::memory_order_relaxed))
{
char buffer[64];
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(buffer, sizeof(buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900, time_info.tm_mon + 1,
time_info.tm_mday, time_info.tm_hour,
time_info.tm_min, time_info.tm_sec);
cached_time = buffer;
last_second.store(now, std::memory_order_release);
}
}
return cached_time;
}
};
// ------------------------------------------------------------------------------
// 主日志类(最外层类)
class Logger
{
private:
// 日志当前级别
std::atomic<LogLevel> currentLevel;
// 存储日志输出目标的数组
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
// 读写锁
std::shared_mutex targets_mutex;
// 缓存时间类
TimeCache time_cache;
private:
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
}
void output_to_targets(const std::string &message)
{
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex);
targets_copy.reserve(outputTargets.size());
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get());
}
}
for (auto target : targets_copy)
{
target->write(message);
}
}
public:
// 删除拷贝构造函数和赋值操作
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
// 虽然单例模式通常不需要,但为了完整性可以添加
Logger(Logger &&) = delete;
Logger &operator=(Logger &&) = delete;
static Logger *getInstance()
{
static Logger instance; // C++11 保证这是线程安全的
return &instance;
}
// 添加日志输出目标到数组中
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
// 先申请一把读写锁
std::unique_lock lock(targets_mutex);
outputTargets.push_back(std::move(outputtarget));
}
// 设置日志的级别
void setLevel(LogLevel level)
{
currentLevel.store(level, std::memory_order_release);
}
// 添加便捷的级别检查方法
bool shouldLog(LogLevel level) const
{
return level >= currentLevel.load(std::memory_order_acquire);
}
LogLevel getLevel() const
{
return currentLevel.load(std::memory_order_acquire);
}
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush();
}
// 也可以考虑刷新控制台输出
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
// 控制台输出通常是行缓冲的,但可以强制刷新
std::cout << std::flush;
}
}
}
// 基础情况:没有更多参数
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归处理参数
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos);
ss << value;
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
}
else
{
// 如果还有剩余参数,输出警告
ss << format;
if constexpr (sizeof...(args) >= 0) // 总是为 true,但确保编译
{
ss << " [EXTRA_ARGS: " << value;
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...);
}
ss << "]";
}
}
}
// 核心日志输出方法--简单信息版
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
// 将日志输出到目标文件
output_to_targets(ss.str());
}
// 核心日志输出方法--格式化信息版
template <typename... Args>
void log(std::string_view filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]"; // 日志发生地点
// 加入格式化日志内容
format_string_impl(std::ref(ss), format, std::forward<Args>(args)...);
// 将日志输出到目标文件
output_to_targets(ss.str());
}
};
// 方案1:统一宏
#define LOG(level, ...) \
do \
{ \
auto logger_instance = Logger::getInstance(); \
if (logger_instance) \
{ \
logger_instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__); \
} \
} while (0)
// 方案2:级别特定的宏
#define TRACE(...) LOG(LogLevel::TRACE, ##__VA_ARGS__)
#define DEBUG(...) LOG(LogLevel::DEBUG, ##__VA_ARGS__)
#define INFO(...) LOG(LogLevel::INFO, ##__VA_ARGS__)
#define WARN(...) LOG(LogLevel::WARN, ##__VA_ARGS__)
#define ERROR(...) LOG(LogLevel::ERROR, ##__VA_ARGS__)
#define FATAL(...) LOG(LogLevel::FATAL, ##__VA_ARGS__)
#endif // LOG_HPP
前提说明
初版代码与最终版代码中间隔了很多代,改了很多版代码后才出的最终版。也正是因为隔了太多代,如果把全部版本都搬出来,那本文会极度冗长,前我也没这个精力,所以我们就着最终版讲解,我会说清楚为什么需要这样做,为什么最终版是这样的。(代码中我认为比较陌生的知识点都已经在番外链接说明了)
一、日志输出目标接口
// 日志输出目标接口
// (日志可以输出到终端上,也可以输出到某个文件上)
class LogOutputTarget
{
public:
virtual ~LogOutputTarget() = default;
virtual void write(std::string_view message) = 0; // 使用string_view
};
// 在终端/控制台打印日志信息 // 将日志内容输出到终端
class Console_OutputTarget : public LogOutputTarget
{
private:
static std::mutex &get_cout_mutex()
{
static std::mutex cout_mutex;
return cout_mutex;
}
public:
void write(std::string_view message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(get_cout_mutex());
std::cout << message << '\n'; // 避免std::endl的flush
}
};
// 将日志内容输出到文件中
class File_OutputTarget : public LogOutputTarget
{
private:
std::ofstream file;
std::string buffer;
static const size_t BUFFER_SIZE = 4096;
std::mutex file_mutex; // 使用独立的mutex (为什么不用静态锁呢?有讲究的,在deek)
std::atomic<bool> destroyed{false};
std::string filename;
public:
File_OutputTarget(const std::string &filename) : filename(filename)
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
throw std::runtime_error("Cannot open log file: " + filename);
}
}
~File_OutputTarget()
{
destroyed.store(true, std::memory_order_release);
flush();
}
void write(std::string_view message) override
{
// 先快速检查,避免不必要的锁竞争
if (destroyed.load(std::memory_order_acquire))
{
return;
}
std::lock_guard<std::mutex> lock(file_mutex);
// 双重检查,防止在等待锁的过程中被销毁
if (destroyed.load(std::memory_order_acquire))
{
return;
}
buffer.append(message);
buffer.push_back('\n');
if (buffer.size() >= BUFFER_SIZE)
{
flush_impl();
}
}
void flush()
{
std::lock_guard<std::mutex> lock(file_mutex);
flush_impl();
}
bool is_open() const
{
return file.is_open();
}
private:
// 为什么没有在这个函数开始执行的时候加锁?
// 因为所有调用这个函数的函数都已经加了锁,就好比如flush()函数,write函数
// 所以就不能再在这个函数中加锁,否则就会形成死锁
// 而且这个函数是私有的,类外是无法调用的,所以无需担心
void flush_impl()
{
if (!file.is_open() || !file.good()) // 同时检查文件状态
{
// 先清除可能的错误状态
file.clear();
// 尝试重新打开文件
try
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
std::cerr << "Failed to reopen log file: " << filename << std::endl;
return;
}
}
catch (const std::exception &e)
{
std::cerr << "Exception reopening log file: " << e.what() << std::endl;
return;
}
}
if (!buffer.empty())
{
try
{
file << buffer;
file.flush();
// 检查写入是否成功
if (!file.good())
{
throw std::runtime_error("File write failed or stream is in bad state");
}
buffer.clear();
}
catch (const std::exception &e)
{
std::cerr << "Log write failed: " << e.what() << std::endl;
// 清空缓冲区,避免重复写入失败的数据
buffer.clear();
}
}
}
};
设计思想:
- 我们想实现:用户能根据使用场景或者个人喜好,来选择日志的输出目标。
- 常见的输出目标为:终端,或者,(专门存储日志内容的)文本文件
- 除了一些常见的输出目标,用户可能还有一些特别的需求,所以我们实现的这个日志输出目标接口,需要有比较好的扩展性。
所以我们就想着通过继承来实现这一部分。
- 接口抽象 -
LogOutputTarget作为抽象基类,定义了统一的日志输出接口 - 多态扩展 - 通过继承实现不同的输出目标,符合开闭原则
- 场景适配 - 针对不同使用场景提供专门的实现
- 扩展性 - 新的输出目标只需继承基类即可轻松添加
( 在 C++ 中,多态扩展中的开闭原则(Open/Closed Principle,OCP) 是面向对象设计的核心原则之一,其核心思想是:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
简单来说,就是当需要为一个系统增加新功能时,应该通过扩展现有代码来实现,而不是修改已有的、已经测试过的代码。)
我们对比一下初版和终版,看看有什么变化:
基类的变化
// 初版
class LogOutputTarget
{
protected:
static std::mutex cout_mutex;
public:
virtual ~LogOutputTarget() = default;
virtual void write(const std::string &message) = 0;
};
// 终版
class LogOutputTarget
{
public:
virtual ~LogOutputTarget() = default;
virtual void write(std::string_view message) = 0; // 使用string_view
};
我们先讲解一个比较小的变化,不宜观察出的:(这个变化在后续的很多函数参数中都有体现)
我们将write函数的参数类型从string变成了–>string_view类型。为什么呢?
string_view的主要优势:
- 零拷贝:不涉及内存分配或数据复制
- 接受更多类型:可以接受
const char*、std::string、字符串字面量等 - 避免临时对象:对于字符串字面量,不需要创建临时
std::string
// 使用 string_view - 高效
write("Hello World"); // 无临时对象
write(std::string("Hello")); // 无额外拷贝
write(some_string); // 无拷贝
// 使用 const string& - 可能低效
write("Hello World"); // 可能创建临时 string 对象
初版为什么会设置一把静态锁
其中一个比较大的变化就是:初版的基类私有成员中有一把静态锁。到了终版代码的基类中则没有了这把静态锁。
为什么呢?
我先说说为什么初版代码的基类中,为什么会有一把静态锁。这把静态锁的目的其实就是为了保护日志输出目标的安全。保证日志信息的正常,防止日志信息的混乱。
在多线程场景的时候,我们使用cout打印信息的时候,常会出现一些信息交错,混乱的现象。常常是两个甚至多个线程打印的信息交织在一起–>线程1打印一半信息,然后线程2在后面也打印一半信息,接着线程1再打印一半信息。
我们就是为了防止出现上述的信息混乱的情况,才选择了在基类中设置了一把静态锁,这把锁就是保护日志输出的目标的(比如终端或者文件)。
为什么撤销
那么为什么接下来我又撤销掉了呢?是不需要锁保护了吗?
回答:我们依旧需要锁保护,但是初版代码的锁保护太简单粗暴了,锁粒度过于大,且不够智能。为什么呢?
首先,无论是输出到终端亦或是输出到文件中的派生类都会继承这一把静态锁。也就是说保护终端和文件的都是同一把锁。
日志输出到终端的同时,无法将内容输出到文件中,但实际上,终端和文件根本就没有联系,完全可以并发执行,日志输出到终端的同时,是可以将日志输出到文件中的。
这就降低了并发度。
其次,终端使用静态锁是较为合理的,因为所有线程共用同一个标准输出(也就是终端嘛,或者理解为屏幕)嘛。所以,所有的类对象得共享同一把锁,才能较好的保护终端,防止输出到终端日志混乱。
但是,文件输出目标类使用静态锁就是不合理的,为什么?
因为多个线程完全有可能将日志输出到不同的文件。所以,如果文件输出目标类使用的是静态锁,那么所有对象共用的就是同一把锁。
那么,就算是不同对象将日志内容输出到不同文件,也需要互斥等待。但这完全是不需要的,文件1和文件2没有任何关系。
将日志内容输出到文件1和将日志内容输出到文件2,完全可以并发进行。
这样一来也降低了并发度。
所以最后我们决定撤销在基类设置的静态锁,而是使用更加灵活的实现方式。
日志输出到终端的派生类
// 初版
class Console_OutputTarget : public LogOutputTarget
{
public:
void write(const std::string &message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << message << std::endl;
}
};
// 终版
class Console_OutputTarget : public LogOutputTarget
{
private:
static std::mutex &get_cout_mutex()
{
static std::mutex cout_mutex;
return cout_mutex;
}
public:
void write(std::string_view message) override
{
// 先申请锁,保护输出
std::lock_guard<std::mutex> lock(get_cout_mutex());
std::cout << message << '\n'; // 避免std::endl的flush
}
};
在终版代码中,对于输出目标为终端的派生类,我们改用静态局部锁。
在 C++11 及以后的标准中,静态局部变量的初始化是线程安全的。编译器会自动为静态局部变量的初始化生成线程安全的代码。
工作流程
- 第一次调用:线程A进入函数,发现静态变量未初始化,获得初始化锁,执行构造
- 并发调用:线程B进入函数,等待初始化完成
- 后续调用:所有线程直接返回已初始化的静态变量引用
优势
- 懒加载:只有在第一次调用时才初始化
- 线程安全:多个线程同时调用不会导致多次初始化
- 自动销毁:程序结束时自动调用析构函数
这一把静态局部锁:
-
一方面很好的保护了终端。
-
另一方面没有影响日志输出到日志,没有降低并发度。
输出方式的小改变:
// 初版
std::cout << message << std::endl;
// 终版
std::cout << message << '\n'; // 避免std::endl的flush
std::endl vs '\n' 的缓冲机制
std::endl 的问题
std::cout << message << std::endl;
等价于:
std::cout << message << '\n';
std::cout.flush(); // 强制刷新缓冲区!
'\n' 的缓冲行为
std::cout << message << '\n'; // 优化版本
缓冲类型分析:
| 缓冲类型 | 刷新条件 | 典型场景 |
|---|---|---|
| 全缓冲 | 缓冲区满 | 文件输出、重定向到文件 |
| 行缓冲 | 遇到\n或缓冲区满 | 终端输出(默认) |
| 无缓冲 | 立即输出 | std::cerr |
对于 std::cout 到终端的情况:
实际情况:
std::cout输出到终端时默认是行缓冲- 使用
'\n'时,遇到换行符就会自动刷新(因为行缓冲) - 但不是等到行缓冲满了才打印!
性能优化的真正原因
// 不好的写法 - 强制刷新
std::cout << message << std::endl;
// 好的写法 - 依靠行缓冲自动管理
std::cout << message << '\n';
优化效果:
- 减少系统调用:
flush()会触发write()系统调用 - 减少上下文切换:频繁系统调用导致用户态/内核态切换
- 利用批量写入:操作系统可以合并多个小写入操作
什么时候需要手动刷新?
// 需要立即看到输出的关键场景
std::cout << "Critical error: " << error_msg << std::endl;
// 调试信息可以延迟输出
std::cout << "Debug: variable = " << value << '\n';
总结:
- 局部静态锁:C++11 保证线程安全的懒加载单例模式
'\n'优化:不是等待缓冲区满,而是避免不必要的强制刷新,让标准库和操作系统更智能地管理输出时机- 实际效果:在高频日志场景下,性能提升显著,因为减少了不必要的系统调用
日志输出到文件的派生类
// 初版
class File_OutputTarget : public LogOutputTarget
{
private:
// std::ofstream 是 C++ 标准库中用于文件输出的类,
// 它继承自 std::ostream,专门用于将数据写入文件。
std::ofstream file;
public:
// 构造函数
File_OutputTarget(const std::string &filename)
{
// 以追加模式(在文件末尾写入)打开名为:filename的文件
file.open(filename, std::ios::app);
}
void write(const std::string &message) override
{
// 先申请锁
std::lock_guard<std::mutex> lock(cout_mutex);
// 先判断文件是否已经打开
if (file.is_open())
{
// 打开后,向文件写入日志内容
file << message << std::endl;
}
}
};
// 终版
class File_OutputTarget : public LogOutputTarget
{
private:
std::ofstream file;
std::string buffer;
static const size_t BUFFER_SIZE = 4096;
std::mutex file_mutex; // 使用独立的mutex
std::atomic<bool> destroyed{false};
std::string filename;
public:
File_OutputTarget(const std::string &filename) : filename(filename)
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
throw std::runtime_error("Cannot open log file: " + filename);
}
}
~File_OutputTarget()
{
destroyed.store(true, std::memory_order_release);
flush();
}
void write(std::string_view message) override
{
// 先快速检查,避免不必要的锁竞争
if (destroyed.load(std::memory_order_acquire))
{
return;
}
std::lock_guard<std::mutex> lock(file_mutex);
// 双重检查,防止在等待锁的过程中被销毁
if (destroyed.load(std::memory_order_acquire))
{
return;
}
buffer.append(message);
buffer.push_back('\n');
if (buffer.size() >= BUFFER_SIZE)
{
flush_impl();
}
}
void flush()
{
std::lock_guard<std::mutex> lock(file_mutex);
flush_impl();
}
bool is_open() const
{
return file.is_open();
}
private:
// 为什么没有在这个函数开始执行的时候加锁?
// 因为所有调用这个函数的函数都已经加了锁,就好比如flush()函数,write函数
// 所以就不能再在这个函数中加锁,否则就会形成死锁
// 而且这个函数是私有的,类外是无法调用的,所以无需担心
void flush_impl()
{
if (!file.is_open() || !file.good()) // 同时检查文件状态
{
// 先清除可能的错误状态
file.clear();
// 尝试重新打开文件
try
{
file.open(filename, std::ios::app);
if (!file.is_open())
{
std::cerr << "Failed to reopen log file: " << filename << std::endl;
return;
}
}
catch (const std::exception &e)
{
std::cerr << "Exception reopening log file: " << e.what() << std::endl;
return;
}
}
if (!buffer.empty())
{
try
{
file << buffer;
file.flush();
// 检查写入是否成功
if (!file.good())
{
throw std::runtime_error("File write failed or stream is in bad state");
}
buffer.clear();
}
catch (const std::exception &e)
{
std::cerr << "Log write failed: " << e.what() << std::endl;
// 清空缓冲区,避免重复写入失败的数据
buffer.clear();
}
}
}
};
我来详细梳理两个版本的区别和优化思路:
🔍 核心区别总览
| 特性 | 初版 | 终版 |
|---|---|---|
| 同步机制 | 使用基类静态锁 | 实例级独立锁 |
| 写入策略 | 立即写入 | 缓冲写入 |
| 性能 | 每次写入都I/O | 批量I/O |
| 线程安全 | 基础保护 | 高级保护(双重检查) |
| 错误处理 | 简单检查 | 完善的异常处理和恢复 |
| 资源管理 | 简单 | RAII + 原子标志 |
🚀 具体优化分析
- 同步机制优化
初版问题:
std::lock_guard<std::mutex> lock(cout_mutex); // 所有文件共享同一把锁!
终版改进:
std::mutex file_mutex; // 每个文件实例(每个派生类对象)独立锁
std::lock_guard<std::mutex> lock(file_mutex);
优势:
- 不同文件可以并发写入
- 终端和文件输出可以并行
- 写入性能优化
初版问题:
file << message << std::endl; // 每次写入都flush!
终版改进:
buffer.append(message);
buffer.push_back('\n');
if (buffer.size() >= BUFFER_SIZE) {
flush_impl(); // 批量写入
}
性能对比:
// 假设有1000条日志,每条100字节
初版:1000次磁盘写入
终版:约2-3次磁盘写入(4096字节缓冲)
- 线程安全增强
终版的双重检查锁定:
void write(std::string_view message) override
{
// 第一重检查:快速路径(无锁)
if (destroyed.load(std::memory_order_acquire)) return;
std::lock_guard<std::mutex> lock(file_mutex);
// 第二重检查:安全确认
if (destroyed.load(std::memory_order_acquire)) return;
// 实际写入操作...
}
内存序的重要性:
// 析构函数中
destroyed.store(true, std::memory_order_release);
// write方法中
if (destroyed.load(std::memory_order_acquire)) return;
这确保了destroyed标志的修改对所有线程立即可见。
- 错误处理和恢复机制
初版:简单检查
if (file.is_open()) {
file << message << std::endl;
}
终版:完善的错误恢复
void flush_impl()
{
if (!file.is_open() || !file.good()) {
file.clear(); // 清除错误状态
file.open(filename, std::ios::app); // 重新打开
if (!file.is_open()) {
std::cerr << "Failed to reopen log file..." << std::endl;
return;
}
}
// 继续写入...
}
处理场景:
- 文件被意外删除
- 磁盘空间不足
- 文件权限变更
- 网络文件系统断开
- 资源管理和RAII
终版的析构函数:
~File_OutputTarget()
{
destroyed.store(true, std::memory_order_release);
flush(); // 确保所有缓冲数据写入文件
}
优势:
- 防止对象销毁后继续写入
- 确保所有日志数据持久化
- 避免数据丢失
📊 性能对比分析
写入性能测试场景
// 模拟高频日志写入
for (int i = 0; i < 10000; ++i) {
target->write("Log message " + std::to_string(i));
}
结果预估:
- 初版: 10000次系统调用,性能瓶颈在磁盘I/O
- 终版: 约25次系统调用(4096字节缓冲),性能提升400倍
内存使用:
- 初版: 内存占用小,但I/O压力大
- 终版: 额外4KB缓冲区,但大幅减少I/O
🛡️ 可靠性对比
- 数据安全性
初版风险:
- 程序崩溃可能丢失最后一条日志
- 无错误恢复机制
终版保障:
- 定期刷新技术减少数据丢失
- 完善的错误恢复机制
- 析构时确保所有数据写入
- 线程安全性
初版: 基础互斥保护
终版:
- 双重检查锁定避免竞争
- 原子操作保证内存可见性
- 安全的对象生命周期管理
🎯 适用场景
- 选择初版的情况
- 日志量很小
- 对性能要求极低
- 代码简洁性优先
- 单线程环境
- 选择终版的情况
- 高性能要求的服务端应用
- 多线程环境
- 需要高可靠性的系统
- 高频日志场景
💡 设计思想演进
从初版到终版的改进体现了:
- 从简单到健壮:基础功能 → 生产级质量
- 性能意识:关注I/O瓶颈和并发性能
- 错误处理:从忽略错误到主动恢复
- 资源管理:完善的RAII和生命周期管理
- 并发安全:高级的线程同步技术
总结: 终版通过缓冲技术、精细的锁设计、完善的错误处理和安全的生命周期管理,在生产环境下的性能、可靠性和稳定性方面都远远优于初版。
二、日志级别信息
// 日志常规信息:日志时间,日志级别
// 日志详细信息:日志时间,日志级别,文件名,行号,进程PID
// 日志级别信息
enum class LogLevel // (使用的是枚举类)
{
TRACE, // 最详细的调试信息
DEBUG, // 调试信息
INFO, // 一般信息
WARN, // 警告信息
ERROR, // 错误信息
FATAL // 严重错误
};
// 将日志级别转换为字符串
// constexpr: 允许编译器计算, 可能完全内联,减少运行时的开销
// noexcept: 告诉编译器不会抛出异常,便于优化
constexpr const char *LevelToString(LogLevel level) noexcept
{
switch (level)
{
case LogLevel::TRACE:
return "[TRACE] ";
case LogLevel::DEBUG:
return "[DEBUG] ";
case LogLevel::INFO:
return "[INFO] ";
case LogLevel::WARN:
return "[WARN] ";
case LogLevel::ERROR:
return "[ERROR] ";
case LogLevel::FATAL:
return "[FATAL] ";
default:
return "[UNKNOWN] ";
}
}
设计思想:
日志相比于常规的cout打印调试信息,日志有一个较大的优势就是:日志有信息级别。我们可以通过控制日志级别来实现在不同环境下,打印对应级别的信息。
就好比如这样:
// 不同环境使用不同级别
class LogConfig {
public:
// 开发环境:显示所有日志
static LogLevel getDevLevel() { return LogLevel::DEBUG; }
// 生产环境:只显示重要日志
static LogLevel getProdLevel() { return LogLevel::INFO; }
};
这个日志级别没什么好说的,这是日志的一个常规信息,每个日志都设置有相应的级别。也没有什么特别的技术。初版和终版的代码也是一样的。
三、日志的时间信息
// 时间格式化缓存优化
class TimeCache
{
private:
std::atomic<time_t> last_second{0};
std::string cached_time;
std::mutex time_mutex;
public:
std::string get_current_time()
{
time_t now = time(nullptr);
time_t last = last_second.load(std::memory_order_acquire);
if (now != last)
{
std::lock_guard<std::mutex> lock(time_mutex);
// 双重检查
if (now != last_second.load(std::memory_order_relaxed))
{
char buffer[64];
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(buffer, sizeof(buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900, time_info.tm_mon + 1,
time_info.tm_mday, time_info.tm_hour,
time_info.tm_min, time_info.tm_sec);
cached_time = buffer;
last_second.store(now, std::memory_order_release);
}
}
return cached_time;
}
};
TimeCache类解析
1. 核心设计思想
class TimeCache
{
private:
std::atomic<time_t> last_second{0}; // 原子变量,记录上次缓存的时间戳
std::string cached_time; // 缓存时间的字符串
std::mutex time_mutex; // 保护缓存的互斥锁
};
2. 工作流程分析
std::string get_current_time()
{
time_t now = time(nullptr);
time_t last = last_second.load(std::memory_order_acquire); // 原子读取
if (now != last) // 快速检查:时间是否变化
{
std::lock_guard<std::mutex> lock(time_mutex);
// 双重检查:防止其他线程已经更新
if (now != last_second.load(std::memory_order_relaxed))
{
// 只有时间真正变化时才重新格式化
char buffer[64];
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(buffer, sizeof(buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900, time_info.tm_mon + 1,
time_info.tm_mday, time_info.tm_hour,
time_info.tm_min, time_info.tm_sec);
cached_time = buffer;
last_second.store(now, std::memory_order_release); // 原子存储
}
}
return cached_time;
}
为什么TimeCache更优
初版
初版的获取时间的方法我是直接集成在log方法里面的,一开始我觉得没必要多封装一个时间类来专门处理时间。
// 核心日志输出方法--简单信息版
void log(std::string filename, int line_num, LogLevel level, const char *message)
{
if (level < currentLevel)
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 用来存储时间信息
char time_buffer[64];
// 获取时间3件套
time_t now = time(nullptr);
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(time_buffer, sizeof(time_buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900,
time_info.tm_mon + 1,
time_info.tm_mday,
time_info.tm_hour,
time_info.tm_min,
time_info.tm_sec);
// 开始格式化日志内容
std::stringstream ss;
ss << time_buffer // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str());
}
}
1. 性能大幅提升
// 旧版:每次调用都要执行所有操作
void log_old_version(...) {
time_t now = time(nullptr); // 系统调用
struct tm time_info; // 栈分配
localtime_r(&now, &time_info); // 时间转换(较重)
snprintf(time_buffer, ...); // 字符串格式化(较重)
// 每秒可能调用数千次,重复执行相同操作
}
// 新版:每秒只执行一次格式化
void log_new_version(...) {
std::string time_str = time_cache.get_current_time(); // 多数情况下直接返回缓存
// 同一秒内的所有日志调用都共享同一个格式化结果
}
2. 减少系统调用开销
time(nullptr):相对轻量,但仍需用户态到内核态切换localtime_r():较重的系统调用,涉及时区计算、闰年判断等复杂逻辑snprintf():格式化字符串需要解析格式说明符,计算缓冲区大小
3. 内存和CPU缓存友好
// 旧版:每次都在栈上创建新缓冲区
char time_buffer[64]; // 每次重新分配,缓存不友好
// 新版:复用缓存字符串
std::string cached_time; // 长期存在,可能被CPU缓存
4. 并发性能优化
// 使用原子操作 + 双重检查锁定模式
if (now != last) { // 无锁快速路径(99.9%的情况)
std::lock_guard<std::mutex> lock(time_mutex); // 有锁慢速路径
if (now != last_second.load(...)) // 双重检查防止竞争
}
5. 内存序优化
// 正确的内存序保证
last_second.load(std::memory_order_acquire); // 保证读到最新值
last_second.store(now, std::memory_order_release); // 保证cached_time对其他线程可见
性能对比分析
假设日志系统在高并发场景下:
| 场景 | 旧版调用次数 | 新版调用次数 | 性能提升 |
|---|---|---|---|
time() 系统调用 | 每秒1000次 | 每秒1次 | 1000倍 |
localtime_r() 调用 | 每秒1000次 | 每秒1次 | 1000倍 |
snprintf() 格式化 | 每秒1000次 | 每秒1次 | 1000倍 |
| 锁竞争 | 无锁但重复计算 | 每秒最多1次锁竞争 | 显著降低 |
总结
TimeCache的优势:
- 极致性能:同一秒内的时间获取几乎零成本
- 减少系统调用:大幅降低内核态切换开销
- 缓存友好:重复利用已格式化的字符串
- 并发优化:无锁设计在绝大多数情况下避免锁竞争
- 内存安全:正确的内存序保证多线程安全性
适用场景:
- 高频日志系统
- 需要精确时间戳的监控系统
- 任何时间格式化频繁调用的场景
四、日志主类
// 主日志类(最外层类)
class Logger
{
private:
// 日志当前级别
std::atomic<LogLevel> currentLevel;
// 存储日志输出目标的数组
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
// 读写锁
std::shared_mutex targets_mutex;
// 缓存时间类
TimeCache time_cache;
private:
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
}
void output_to_targets(const std::string &message)
{
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex);
targets_copy.reserve(outputTargets.size());
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get());
}
}
for (auto target : targets_copy)
{
target->write(message);
}
}
public:
// 删除拷贝构造函数和赋值操作
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
// 虽然单例模式通常不需要,但为了完整性可以添加
Logger(Logger &&) = delete;
Logger &operator=(Logger &&) = delete;
static Logger *getInstance()
{
static Logger instance; // C++11 保证这是线程安全的
return &instance;
}
// 添加日志输出目标到数组中
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
// 先申请一把读写锁
std::unique_lock lock(targets_mutex);
outputTargets.push_back(std::move(outputtarget));
}
// 设置日志的级别
void setLevel(LogLevel level)
{
currentLevel.store(level, std::memory_order_release);
}
// 添加便捷的级别检查方法
bool shouldLog(LogLevel level) const
{
return level >= currentLevel.load(std::memory_order_acquire);
}
LogLevel getLevel() const
{
return currentLevel.load(std::memory_order_acquire);
}
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush();
}
// 也可以考虑刷新控制台输出
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
// 控制台输出通常是行缓冲的,但可以强制刷新
std::cout << std::flush;
}
}
}
// 基础情况:没有更多参数
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归处理参数
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos);
ss << value;
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
}
else
{
// 如果还有剩余参数,输出警告
ss << format;
if constexpr (sizeof...(args) >= 0) // 总是为 true,但确保编译
{
ss << " [EXTRA_ARGS: " << value;
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...);
}
ss << "]";
}
}
}
// 核心日志输出方法--简单信息版
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
// 将日志输出到目标文件
output_to_targets(ss.str());
}
// 核心日志输出方法--格式化信息版
template <typename... Args>
void log(std::string_view filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (!shouldLog(level))
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 开始格式化日志内容
std::stringstream ss;
ss << time_cache.get_current_time() // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]"; // 日志发生地点
// 加入格式化日志内容
format_string_impl(std::ref(ss), format, std::forward<Args>(args)...);
// 将日志输出到目标文件
output_to_targets(ss.str());
}
};
1. 整体架构设计
设计思想
这是一个单例模式的线程安全日志系统,采用多输出目标、级别过滤、性能优化的综合设计。
class Logger
{
private:
// 控制核心:级别控制 + 输出目标管理 + 时间优化
std::atomic<LogLevel> currentLevel; // 原子控制 日志级别
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets; // 多日志输出目标存储
std::shared_mutex targets_mutex; // 读写分离锁
TimeCache time_cache; // 时间格式化优化
};
2. 单例模式实现分析
现代C++单例模式
// 构造函数私有化,防止外部实例化
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level) {}
// 删除所有拷贝和移动操作,确保唯一性
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
Logger(Logger &&) = delete;
Logger &operator=(Logger &&) = delete;
// C++11保证的线程安全单例
static Logger *getInstance()
{
static Logger instance; // 首次调用时构造,线程安全
return &instance;
}
设计优点:
- 线程安全:C++11标准保证静态局部变量的初始化是线程安全的
- 懒加载:只有在第一次调用时才创建实例
- 自动析构:程序结束时自动调用析构函数
- 防止误用:删除拷贝构造,避免意外复制
3. 线程安全设计
3.1 原子操作的应用
// 使用std::atomic实现无锁的日志级别控制
std::atomic<LogLevel> currentLevel;
// 设置日志的级别
void setLevel(LogLevel level)
{
// 设置当前的日志级别,方便后续过滤无关的日志
// memory_order_release:保证之前的写操作对读取线程可见
currentLevel.store(level, std::memory_order_release);
}
// 便捷的级别检查方法
bool shouldLog(LogLevel level) const
{
// 判断此日志的级别是否大于用户设置好了的日志级别,如果小于,则过滤掉
// 如果大于等于,则正常输出日志
// memory_order_acquire:保证读取到最新的设置值
return level >= currentLevel.load(std::memory_order_acquire);
}
// 获取当前的日志级别
LogLevel getLevel() const
{
// 原子读取操作
// memory_order_acquire:保证读取到最新的设置值
return currentLevel.load(std::memory_order_acquire);
}
内存序选择的意义:
memory_order_acquire:确保后续的读操作不会重排到该加载之前memory_order_release:确保之前的写操作不会重排到该存储之后- 形成同步关系,保证级别变化的可见性
3.2 读写锁的精细控制–>日志输出
// 写操作:添加输出目标(不频繁)
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
std::unique_lock lock(targets_mutex); // 排他锁
outputTargets.push_back(std::move(outputtarget));
}
// 读操作:输出日志(非常频繁)
void output_to_targets(const std::string &message)
{
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex); // 共享锁,允许多线程并发读
targets_copy.reserve(outputTargets.size());
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get());
}
} // 锁范围结束,后续操作无锁
// 无锁状态下进行实际的IO操作
for (auto target : targets_copy)
{
target->write(message);
}
}
读写锁的优势:
- 读多写少:日志输出是高频读操作,目标配置是低频写操作
- 并发提升:多个日志线程可以同时读取输出目标列表
- 减少竞争:实际IO操作在无锁状态下进行
这个函数在后续有详细讲解。
4. 性能优化策略
4.1 时间格式化缓存
// 在log方法中使用缓存的时间
ss << time_cache.get_current_time() // 同一秒内零成本获取
优化效果:
- 将
localtime_r和snprintf从每次调用变为每秒一次 - 避免重复的系统调用和字符串格式化
4.2 级别检查前置
template <typename... Args>
void log(std::string_view filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (!shouldLog(level)) // 快速检查,避免不必要的格式化
{
return;
}
// 只有在需要输出时才进行昂贵的格式化操作
}
避免的开销:
- 字符串流构造
- 参数包展开
- 格式化处理
- 时间获取(即使有缓存)
4.3 字符串视图的使用
std::string_view就是字符串视图。
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
优势:
- 避免
std::string的拷贝构造 - 兼容C字符串和
std::string - 轻量级的字符串引用
5. 格式化系统设计
5.1 类型安全的格式化
// 基础情况:递归终止
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归展开参数包
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos); // 输出{}之前的部分
ss << value; // 输出参数值
// 递归处理剩余部分和剩余参数
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
}
else
{
// 处理参数过多的情况
ss << format;
if constexpr (sizeof...(args) >= 0)
{
ss << " [EXTRA_ARGS: " << value;
// C++17折叠表达式输出剩余参数
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...);
}
ss << "]";
}
}
}
这个函数在后文有详细讲解。
设计亮点:
- 编译时多态:模板递归处理任意类型和数量的参数
- 完美转发:
std::forward保持参数的值类别 - 错误处理:参数数量不匹配时给出明确提示
- 类型安全:编译期检查,避免
printf风格的类型错误
5.2 现代C++特性应用
// C++17的if constexpr,编译期条件判断
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...); // 折叠表达式
}
6. 输出目标管理
6.1 多目标支持
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
// 支持同时输出到文件、控制台、网络等
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
std::unique_lock lock(targets_mutex);
outputTargets.push_back(std::move(outputtarget)); // 所有权转移
}
设计优势:
- 多态支持:通过基类指针支持不同类型的输出目标
- 资源管理:
unique_ptr自动管理生命周期 - 灵活扩展:易于添加新的输出目标类型
6.2 刷新机制
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush(); // 文件目标强制刷新
}
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
std::cout << std::flush; // 控制台刷新
}
}
}
这个函数在后续有详细讲解。
刷新策略:
- 类型识别:
dynamic_cast识别具体输出类型 - 差异处理:不同输出目标采用不同的刷新方式
- 线程安全:在锁保护下遍历目标列表
7. 日志方法设计
7.1 两个重载版本
两个版本的log方法的执行逻辑我已经在代码里注释的很清楚了。
// 简单版本:直接输出字符串
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
// 格式化版本:支持参数包
template <typename... Args>
void log(std::string_view filename, int line_num, LogLevel level, const char *format, Args &&...args)
设计考虑:
- 性能优化:简单字符串避免格式化开销
- 灵活性:格式化版本支持复杂输出
- API统一:相同的调用接口
7.2 调用信息收集
// 包含文件名和行号,便于定位日志来源
"[" << filename << ":" << line_num << "]"
调试支持:
- 使用
__FILE__和__LINE__宏自动捕获调用位置 - 便于问题排查和日志追踪
8. 整体设计总结
8.1 性能优化
- 时间缓存:大幅减少时间格式化开销
- 级别过滤:尽早拒绝不需要的日志
- 读写分离:减少锁竞争
- 零拷贝:使用
string_view避免字符串复制
8.2 线程安全
- 原子级别:无锁的级别控制
- 读写锁:读操作并发,写操作互斥
- 锁范围最小化:IO操作在无锁状态下进行
- 内存序正确:保证多线程可见性
8.3 扩展性
- 多输出目标:支持同时输出到多个目的地
- 格式化系统:支持任意类型和数量的参数
- 级别管理:动态调整日志详细程度
- 单例模式:全局统一的日志入口
8.4 现代C++特性
- RAII:自动资源管理
- 移动语义:避免不必要的拷贝
- 模板元编程:类型安全的格式化
- 内存模型:正确的多线程同步
五、日志之类初版和终版的对比
初版日志主类
// 主日志类(最外层类)
class Logger
{
private:
// 日志当前级别
LogLevel currentLevel;
// 存储日志输出目标的数组
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
public:
// 静态实例指针,用于宏访问
static Logger *instance;
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
instance = this;
}
// 添加日志输出目标到数组中
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
outputTargets.push_back(std::move(outputtarget));
}
// 设置日志的级别
void setLevel(LogLevel level)
{
currentLevel = level;
}
// 基础情况:没有更多参数
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归处理参数
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos); // 输出 {} 之前的部分
ss << value; // 输出参数值
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...); // 处理剩余部分
}
else
{
ss << format; // 没有找到 {},直接输出整个字符串
}
}
// 核心日志输出方法--简单信息版
void log(std::string filename, int line_num, LogLevel level, const char *message)
{
if (level < currentLevel)
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 用来存储时间信息
char time_buffer[64];
// 获取时间3件套
time_t now = time(nullptr);
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(time_buffer, sizeof(time_buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900,
time_info.tm_mon + 1,
time_info.tm_mday,
time_info.tm_hour,
time_info.tm_min,
time_info.tm_sec);
// 开始格式化日志内容
std::stringstream ss;
ss << time_buffer // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]" // 日志发生地点
<< message; // 日志内容
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str());
}
}
// 核心日志输出方法--格式化信息版
template <typename... Args>
void log(std::string filename, int line_num, LogLevel level, const char *format, Args &&...args)
{
if (level < currentLevel)
{
return; // 低于当前级别的日志不输出(为什么?)
}
// 用来存储时间信息
char time_buffer[64];
// 获取时间3件套
time_t now = time(nullptr);
struct tm time_info;
localtime_r(&now, &time_info);
snprintf(time_buffer, sizeof(time_buffer), "[%04d-%02d-%02d %02d:%02d:%02d] ",
time_info.tm_year + 1900,
time_info.tm_mon + 1,
time_info.tm_mday,
time_info.tm_hour,
time_info.tm_min,
time_info.tm_sec);
// 开始格式化日志内容
std::stringstream ss;
ss << time_buffer // 日志时间
<< LevelToString(level) // 日志级别
<< "[" << getpid() << "] " // 进程PID
<< "[" << filename << ":" << line_num << "]"; // 日志发生地点
// 加入日志内容
format_string_impl(std::ref(ss), format, std::forward<Args>(args)...);
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str());
}
}
};
// 静态成员定义
Logger *Logger::instance = nullptr;
1. 单例模式实现对比
初版:手动单例(问题严重)
// 静态实例指针,用于宏访问
static Logger *instance;
// 构造函数
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level)
{
instance = this; // 问题:多个实例会互相覆盖
}
// 静态成员定义
Logger *Logger::instance = nullptr;
问题分析:
- 非真正单例:可以创建多个Logger实例,互相覆盖
instance指针 - 线程不安全:静态指针初始化存在竞态条件
- 构造顺序问题:全局对象构造顺序不确定
终版:现代单例(正确实现)
// 构造函数私有化
Logger(LogLevel level = LogLevel::INFO) : currentLevel(level) {}
// 删除所有拷贝和移动操作
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
Logger(Logger &&) = delete;
Logger &operator=(Logger &&) = delete;
static Logger *getInstance()
{
static Logger instance; // C++11保证线程安全
return &instance;
}
优势:
- 真正单例:无法创建多个实例
- 线程安全:C++11保证静态局部变量线程安全
- 懒加载:首次调用时才创建
- 自动析构:程序退出时自动清理
2. 线程安全性对比
初版:完全非线程安全
// 无任何同步机制
LogLevel currentLevel; // 普通变量,多线程读写数据竞争
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets; // 并发修改导致未定义行为
void setLevel(LogLevel level) { currentLevel = level; } // 非原子操作
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
outputTargets.push_back(std::move(outputtarget)); // 并发修改vector危险
}
风险:
- 数据竞争:多线程读写
currentLevel导致未定义行为 - 容器损坏:并发修改
outputTargets可能导致内存损坏 - 级别不一致:不同线程看到不同的日志级别
终版:全面线程安全
// 原子操作 + 读写锁
std::atomic<LogLevel> currentLevel;
std::vector<std::unique_ptr<LogOutputTarget>> outputTargets;
std::shared_mutex targets_mutex;
void setLevel(LogLevel level)
{
currentLevel.store(level, std::memory_order_release); // 原子存储
}
bool shouldLog(LogLevel level) const
{
return level >= currentLevel.load(std::memory_order_acquire); // 原子加载
}
void addOutputTarget(std::unique_ptr<LogOutputTarget> outputtarget)
{
std::unique_lock lock(targets_mutex); // 写锁保护
outputTargets.push_back(std::move(outputtarget));
}
安全保证:
- 原子级别控制:无锁的级别检查
- 读写分离:读操作并发,写操作互斥
- 内存序正确:保证多线程可见性
3. 性能优化对比
初版:性能低下
void log(std::string filename, int line_num, LogLevel level, const char *message)
{
if (level < currentLevel) return; // 检查太晚,已传递了所有参数
// 每次调用都要执行昂贵的操作
char time_buffer[64];
time_t now = time(nullptr); // 系统调用
struct tm time_info;
localtime_r(&now, &time_info); // 较重的时间转换
snprintf(time_buffer, ...); // 字符串格式化
// 直接遍历输出目标(可能持有锁时间过长)
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str()); // 每个目标都重新生成字符串
}
}
性能瓶颈:
- 重复时间格式化:每次日志调用都执行
localtime_r和snprintf - 锁竞争:无锁保护,但并发修改危险
- 字符串复制:多次生成相同的时间字符串
终版:极致优化
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
{
if (!shouldLog(level)) return; // 尽早拒绝,避免后续开销
std::stringstream ss;
ss << time_cache.get_current_time() // 时间缓存,同一秒内零成本
<< LevelToString(level)
<< "[" << getpid() << "] "
<< "[" << filename << ":" << line_num << "]"
<< message;
output_to_targets(ss.str()); // 分离IO操作
}
void output_to_targets(const std::string &message)
{
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex); // 短暂持有读锁
// 快速复制指针列表
targets_copy.reserve(outputTargets.size());
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get());
}
} // 锁释放
// 无锁状态下执行IO操作
for (auto target : targets_copy)
{
target->write(message); // 所有目标共享同一个字符串
}
}
性能优势:
- 时间缓存:每秒只格式化一次时间
- 尽早拒绝:在参数展开前检查级别
- 锁范围最小化:IO操作在无锁状态下执行
- 零拷贝优化:使用
string_view避免字符串复制
解释一下尽早拒绝:
- 相对于参数构造:终版使用
string_view避免了std::string的拷贝构造 - 相对于格式化操作:检查在
format_string_impl调用之前 - 相对于IO操作:检查在所有输出操作之前
严格来说,两个版本都不是真正的"尽早",因为:
- 参数在函数调用前已经构造完成
- 函数调用本身的开销无法避免
真正的优化在于:
- 终版减少了不必要的字符串拷贝
- 终版有更好的缓存和并发性能
所以上面的"尽早拒绝"更多是相对于函数内部的其他昂贵操作而言,而不是相对于参数传递。这是一个相对的概念,不是绝对的"最早可能时机"。
4. 内存和资源管理对比
初版:资源管理粗糙
// 使用std::string filename,可能产生不必要的拷贝
void log(std::string filename, int line_num, LogLevel level, const char *message)
// 无刷新机制,可能丢失日志
终版:精细资源管理
// 使用string_view,避免拷贝
void log(std::string_view filename, int line_num, LogLevel level, const char *message)
// 提供刷新接口
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush(); // 文件刷新
}
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
std::cout << std::flush; // 控制台刷新
}
}
}
5. 错误处理和健壮性对比
初版:错误处理薄弱
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
// 正常处理
}
else
{
ss << format; // 没有找到 {},直接输出整个字符串
// 忽略了多余的参数,可能导致信息丢失
}
}
终版:完善的错误处理
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
// 正常处理
}
else
{
// 如果还有剩余参数,输出警告
ss << format;
if constexpr (sizeof...(args) >= 0)
{
ss << " [EXTRA_ARGS: " << value;
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...); // C++17折叠表达式
}
ss << "]";
}
}
}
改进:
- 参数不匹配警告:明确提示格式化错误
- 现代C++特性:使用折叠表达式处理剩余参数
6. API设计对比
初版:API设计不完整
// 缺少级别查询方法
// 缺少刷新接口
// 单例访问方式不安全
// 使用示例:
Logger logger;
logger.instance->log(...); // 容易出错
终版:完整且安全的API
// 完整的级别管理
bool shouldLog(LogLevel level) const;
LogLevel getLevel() const;
void setLevel(LogLevel level);
// 资源管理
void flush_all();
// 安全的单例访问
static Logger *getInstance();
// 使用示例:
Logger::getInstance()->log(...); // 安全可靠
7. 总结对比
初版的问题总结:
- 线程不安全:完全不能在多线程环境中使用
- 性能低下:重复的时间格式化和系统调用
- 设计缺陷:伪单例模式,容易误用
- 资源泄露风险:无刷新机制,可能丢失日志
- 错误处理薄弱:参数不匹配时静默忽略
终版的优势总结:
- 完全线程安全:原子操作+读写锁,适合高并发
- 极致性能:时间缓存+尽早拒绝+锁优化
- 现代C++:使用原子内存序、string_view、折叠表达式等
- 健壮设计:真正的单例,完整的API,错误处理
- 可维护性:代码结构清晰,职责分离
性能数据预估(假设每秒1000条日志):
| 操作 | 初版开销 | 终版开销 | 提升倍数 |
|---|---|---|---|
time() 调用 | 1000次/秒 | 1次/秒 | 1000倍 |
localtime_r() 调用 | 1000次/秒 | 1次/秒 | 1000倍 |
| 时间字符串格式化 | 1000次/秒 | 1次/秒 | 1000倍 |
| 锁竞争 | 无锁但数据危险 | 几乎无竞争 | 安全性提升 |
| 字符串拷贝 | 多次拷贝 | 零拷贝优化 | 显著减少 |
结论: 终版在保持相同功能的前提下,在性能、安全性、健壮性方面都有数量级的提升,是一个真正适合生产环境的高性能日志系统。
六、宏封装
// 统一宏
#define LOG(level, ...) \
do \
{ \
auto logger_instance = Logger::getInstance(); \
if (logger_instance) \
{ \
logger_instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__); \
} \
} while (0)
// 级别特定的宏
#define TRACE(...) LOG(LogLevel::TRACE, ##__VA_ARGS__)
#define DEBUG(...) LOG(LogLevel::DEBUG, ##__VA_ARGS__)
#define INFO(...) LOG(LogLevel::INFO, ##__VA_ARGS__)
#define WARN(...) LOG(LogLevel::WARN, ##__VA_ARGS__)
#define ERROR(...) LOG(LogLevel::ERROR, ##__VA_ARGS__)
#define FATAL(...) LOG(LogLevel::FATAL, ##__VA_ARGS__)
1. 宏设计的整体架构
1.1 设计目标
// 目标:提供简洁、类型安全、高性能的日志调用接口
// 从这样:
logger->log("file.cpp", 123, LogLevel::INFO, "User {} logged in", user_id);
// 变成这样:
INFO("User {} logged in", user_id);
1.2 宏架构层次
TRACE/DEBUG/INFO/WARN/ERROR/FATAL ← 用户直接调用
↓
LOG ← 统一处理层
↓
Logger::log() ← 实际实现层
2. 统一宏 LOG 详细解析
2.1 do { ... } while (0) 惯用法
#define LOG(level, ...) \
do \
{ \
/* 宏体 */ \
} while (0)
为什么这样设计?
2.1.1 避免分号问题
// 错误示例:不使用 do-while
#define LOG(level, ...) { /* code */ }
// 使用场景:
if (condition)
LOG(INFO, "message"); // 正确
else
do_something(); // 错误:else不能跟在}后面
// 使用 do-while(0) 解决:
if (condition)
LOG(INFO, "message"); // 展开后:do { ... } while(0);
else
do_something(); // 正确:语法合法
2.1.2 保证语句完整性
// 确保宏展开后是一个完整的语句
LOG(INFO, "test"); // 展开为:do { ... } while(0);
2.1.3 必须使用分号
LOG(INFO, "test") // 错误:缺少分号
LOG(INFO, "test"); // 正确:完整的语句
2.2 单例实例获取与空指针检查
auto logger_instance = Logger::getInstance();
if (logger_instance)
{
// 只有在logger存在时才调用log方法
}
设计考虑:
2.2.1 避免空指针解引用
// 如果不检查:
Logger::getInstance()->log(...); // 如果getInstance()返回nullptr,程序崩溃
// 检查后:
if (logger_instance) { ... } // 安全,即使单例未初始化也不会崩溃
2.2.2 错误恢复能力
// 在以下情况下getInstance()可能返回nullptr:
// - 静态初始化顺序问题
// - 内存不足
// - 程序退出阶段
// 宏设计确保在这些情况下不会崩溃,只是静默跳过日志记录
2.3 预处理器宏的使用
logger_instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__)
2.3.1 __FILE__ 和 __LINE__
// 预定义的宏,由编译器自动提供
__FILE__ // 当前源文件名(字符串字面量)
__LINE__ // 当前行号(整数字面量)
// 示例:
INFO("test"); // 在main.cpp第42行调用
// 展开为:log("main.cpp", 42, LogLevel::INFO, "test")
2.3.2 ##__VA_ARGS__ 技巧
// ## 的作用:当可变参数为空时,消除前面的逗号
LOG(INFO, "message") // 展开:log(__FILE__, __LINE__, INFO, "message")
LOG(INFO, "format: {}", 42) // 展开:log(__FILE__, __LINE__, INFO, "format: {}", 42)
// 如果没有 ##:
LOG(INFO, "message") // 错误:log(__FILE__, __LINE__, INFO, "message",) 多了一个逗号
3. 级别特定宏的设计
3.1 语法糖设计
#define TRACE(...) LOG(LogLevel::TRACE, ##__VA_ARGS__)
#define DEBUG(...) LOG(LogLevel::DEBUG, ##__VA_ARGS__)
#define INFO(...) LOG(LogLevel::INFO, ##__VA_ARGS__)
#define WARN(...) LOG(LogLevel::WARN, ##__VA_ARGS__)
#define ERROR(...) LOG(LogLevel::ERROR, ##__VA_ARGS__)
#define FATAL(...) LOG(LogLevel::FATAL, ##__VA_ARGS__)
优势:
3.1.1 使用简洁
// 不需要记住枚举值
INFO("System started"); // 清晰直观
DEBUG("User {} connected", user_id); // 自动识别级别
ERROR("Database connection failed: {}", e); // 错误信息明确
3.1.2 编译时级别确定
// 宏在编译时展开,级别信息是编译期常量
INFO("test"); // 编译为:LogLevel::INFO(编译期已知)
3.2 完整的调用链示例
// 用户调用:
INFO("User {} logged in from {}", username, ip_address);
// 宏展开过程:
INFO("User {} logged in from {}", username, ip_address)
↓
LOG(LogLevel::INFO, "User {} logged in from {}", username, ip_address)
↓
do {
auto logger_instance = Logger::getInstance();
if (logger_instance) {
logger_instance->log(__FILE__, __LINE__,
LogLevel::INFO,
"User {} logged in from {}",
username, ip_address);
}
} while (0)
4. 设计优势分析
4.1 类型安全与性能
// 对比printf风格:
printf("Error: %s\n", error_msg); // 类型不安全,可能崩溃
// 对比流式输出:
std::cout << "Error: " << error_msg << std::endl; // 类型安全但冗长
// 本设计:
ERROR("Error: {}", error_msg); // 类型安全且简洁
4.2 零运行时开销的元信息
// __FILE__ 和 __LINE__ 是编译期常量,零运行时成本
// 但提供了宝贵的调试信息:
[2024-01-15 10:30:00] INFO [1234] [main.cpp:42] User john logged in
// ↑ 精确的代码位置
4.3 灵活的格式化支持
// 支持各种调用方式:
TRACE("Simple message"); // 无参数
DEBUG("Value: {}", 42); // 一个参数
INFO("User: {}, Age: {}, Active: {}", name, age, true); // 多个参数
WARN("Config: {}, Path: {}", config_value, get_path()); // 函数调用参数
5. 潜在问题与改进
5.1 编译器兼容性
// ##__VA_ARGS__ 是GCC/Clang扩展,在MSVC中可能不同
// 可移植版本:
#ifdef _MSC_VER
#define LOG(level, ...) /* MSVC版本 */
#else
#define LOG(level, ...) /* GCC/Clang版本 */
#endif
5.2 性能优化空间
// 当前:总是获取单例实例
auto logger_instance = Logger::getInstance();
// 优化:先检查级别,再获取实例(需要额外设计)
#define LOG_OPTIMIZED(level, ...) \
do { \
if (Logger::isLevelEnabled(level)) { \
auto logger_instance = Logger::getInstance(); \
if (logger_instance) { \
logger_instance->log(__FILE__, __LINE__, level, ##__VA_ARGS__); \
} \
} \
} while (0)
6. 实际使用示例
6.1 基本用法
void process_user(const std::string& username) {
TRACE("Entering process_user: {}", username);
if (username.empty()) {
WARN("Empty username provided");
return;
}
INFO("Processing user: {}", username);
try {
// 业务逻辑
DEBUG("User data loaded: {} records", load_user_data(username));
} catch (const std::exception& e) {
ERROR("Failed to process user {}: {}", username, e.what());
}
TRACE("Exiting process_user: {}", username);
}
6.2 错误处理
bool connect_to_database(const std::string& connection_string) {
INFO("Attempting database connection: {}", connection_string);
if (connection_string.empty()) {
ERROR("Empty connection string provided");
return false;
}
auto connection = create_connection(connection_string);
if (!connection) {
FATAL("Failed to create database connection");
return false;
}
INFO("Database connection established successfully");
return true;
}
7. 总结
这个宏设计体现了现代C++日志库的最佳实践:
- 简洁API:通过宏隐藏复杂的参数传递
- 编译期优化:
__FILE__和__LINE__零运行时成本 - 类型安全:基于模板的格式化,避免类型错误
- 健壮性:空指针检查和错误恢复
- 可读性:级别特定的宏让代码意图清晰
- 性能:尽早的级别检查和高效的参数传递
这种设计让日志记录变得既简单又强大,是生产级日志系统的典型实现方式。
output_to_targets函数
1. 函数执行逻辑分解
void output_to_targets(const std::string &message)
{
// 步骤1:创建临时指针容器
std::vector<LogOutputTarget *> targets_copy;
// 步骤2:在锁保护下快速复制指针
{
std::shared_lock lock(targets_mutex); // 获取读锁
targets_copy.reserve(outputTargets.size()); // 预分配内存
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get()); // 复制裸指针
}
} // 锁自动释放
// 步骤3:无锁状态下执行IO操作
for (auto target : targets_copy)
{
target->write(message); // 调用输出方法
}
}
步骤3中的无锁状态指的是:无targets_mutex锁。
targets_mutex锁保护的是outputTargets(存储日志输出目标的数组)。保护日志输出目标的锁在日志输出目标的派生类里,所以日志输出是安全的。
2. 为什么不直接遍历输出?
直接遍历的问题:
// 问题版本:
for (auto &outputarget : outputTargets)
{
outputarget->write(ss.str()); // 直接在锁内执行IO
}
这样设计会有以下严重问题:
2.1 锁持有时间过长
// 直接遍历:锁持有时间 = 所有IO操作的总时间
{
std::shared_lock lock(targets_mutex); // 获取锁
for (auto &target : outputTargets) // 开始遍历
{
target->write(message); // ⚠️ 每个write都可能很慢!
// 文件写入可能阻塞
// 网络输出可能等待
// 控制台输出可能因终端慢而阻塞
}
} // 锁释放
锁竞争分析:
- 线程A:持有锁,执行文件写入(可能耗时10ms)
- 线程B:等待锁,无法添加新的输出目标
- 线程C:等待锁,无法输出其他日志
- 所有操作串行化,并发性能急剧下降
2.2 并发修改风险
// 即使使用锁,长时间持有仍可能导致问题
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets) // 开始迭代
{
// 如果此时其他线程修改了outputTargets?
// 比如删除了某个目标?
target->write(message); // 可能访问已删除对象!
}
}
3. 设计优势详细分析
3.1 锁范围最小化
// 优化版本:锁只保护指针复制
{
std::shared_lock lock(targets_mutex); // 获取锁
targets_copy.reserve(outputTargets.size()); // 预分配(快速)
for (auto &target : outputTargets) // 复制指针(快速)
{
targets_copy.push_back(target.get()); // 只是指针赋值
}
} // 立即释放锁 - 锁持有时间极短!
// 后续IO操作在无锁状态下进行
for (auto target : targets_copy) // 使用副本,安全
{
target->write(message); // 慢操作,但无锁
}
锁持有时间对比:
- 直接遍历:锁持有时间 = ∑(每个目标的write时间)
- 优化版本:锁持有时间 = 指针复制时间(通常<1ms)
3.2 真正的读写并发
// 场景:3个线程同时输出日志
// 线程1:复制指针列表(持有读锁) → 执行IO(无锁)
// 线程2:复制指针列表(持有读锁) → 执行IO(无锁)
// 线程3:添加输出目标(等待写锁) → 修改列表
// 结果:线程1和线程2可以并发复制指针,并发执行IO
// 线程3只在真正修改时短暂等待
3.3 异常安全性
// 如果某个target->write()抛出异常
for (auto target : targets_copy)
{
try {
target->write(message);
} catch (...) {
// 异常不会影响其他目标的输出
// 也不会破坏outputTargets容器
// 因为我们在操作副本!
}
}
对比直接遍历:
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
target->write(message); // 异常可能导致锁无法释放!
}
} // 如果异常发生在锁内,锁可能无法正常释放
4. 性能影响分析
高并发场景模拟
假设:
- 5个输出目标(文件、控制台、网络等)
- 每个
write()平均耗时2ms - 10个线程同时输出日志
直接遍历的性能:
锁持有时间 = 5个目标 × 2ms = 10ms
吞吐量 = 1个线程/10ms = 100请求/秒
优化版本的性能:
锁持有时间 ≈ 指针复制时间 ≈ 0.1ms
吞吐量 = 10个线程并发 × (1000ms/0.1ms) ≈ 100,000请求/秒
5. 内存和生命周期考量
为什么使用裸指针副本?
std::vector<LogOutputTarget *> targets_copy;
for (auto &target : outputTargets)
{
targets_copy.push_back(target.get()); // 获取原始指针
}
设计考虑:
- 轻量级:只复制指针,不涉及对象拷贝
- 生命周期安全:
unique_ptr保证对象在outputTargets存在期间有效 - 性能:避免智能指针的引用计数操作
生命周期保证
// 在output_to_targets执行期间:
// - targets_copy中的指针指向的对象由unique_ptr管理
// - 只要不调用addOutputTarget/修改outputTargets,对象就有效
// - 由于我们很快释放了锁,修改操作可以很快进行
6. 实际执行流程示例
// 假设有3个输出目标:文件、控制台、网络
void output_to_targets(const std::string &message)
{
// 阶段1:快速复制(锁保护,<1ms)
std::vector<LogOutputTarget *> targets_copy;
{
std::shared_lock lock(targets_mutex); // 获取读锁
targets_copy = {file_ptr, console_ptr, network_ptr}; // 实际复制
} // 立即释放锁
// 阶段2:并发友好IO(无锁,可能较慢)
file_ptr->write(message); // 可能阻塞
console_ptr->write(message); // 可能阻塞
network_ptr->write(message); // 可能阻塞
// 其他线程在此期间可以自由:
// - 输出其他日志(调用output_to_targets)
// - 添加新的输出目标(等待写锁)
}
7. 总结
这样设计的核心思想:
- 分离关注点:数据访问保护 vs IO操作执行
- 锁范围最小化:只保护真正需要同步的临界区
- 提升并发性:允许IO操作并行执行
- 异常安全:避免锁内的异常导致资源泄漏
直接遍历的弊端:
- 锁竞争严重,并发性能差
- 异常安全性差
- 响应性差(长时间持有锁)
优化版本的优势:
- 高并发,低锁竞争
- 更好的异常安全性
- 更好的系统响应性
- 更适合真实世界的IO操作(通常较慢且不可预测)
这是一个典型的"复制-然后-操作"模式,在需要保护共享数据但操作本身较慢的场景中非常有用。
format_string_impl函数
// 基础情况:递归终止
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format;
}
// 递归展开参数包
template <typename T, typename... Args>
void format_string_impl(std::stringstream &ss, const std::string &format, T &&value, Args &&...args)
{
size_t pos = format.find("{}");
if (pos != std::string::npos)
{
ss << format.substr(0, pos); // 输出{}之前的部分
ss << value; // 输出参数值
// 递归处理剩余部分和剩余参数
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
}
else
{
// 处理参数过多的情况
ss << format;
if constexpr (sizeof...(args) >= 0)
{
ss << " [EXTRA_ARGS: " << value;
// C++17折叠表达式输出剩余参数
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...);
}
ss << "]";
}
}
}
1. 整体设计思想
这是一个编译时递归模板实现的字符串格式化系统,模仿了现代C++库(如fmtlib)和Python的str.format()的设计理念。
核心目标:
- 类型安全的字符串格式化(避免
printf的类型不匹配问题) - 支持任意数量和类型的参数
- 使用简单的
{}作为占位符 - 编译时参数包展开
2. 函数执行逻辑详细分解
2.1 基础情况(递归终止)
inline void format_string_impl(std::stringstream &ss, const std::string &format)
{
ss << format; // 没有更多参数,直接输出剩余格式字符串
}
作用:
- 当所有参数都已处理完毕时调用
- 输出格式字符串中剩余的字符
- 结束递归过程
2.2 递归情况(参数处理)
第一步:查找占位符
size_t pos = format.find("{}");
执行逻辑:
- 在当前格式字符串中查找第一个
{}出现的位置 pos的值:std::string::npos:未找到(特殊值,通常是-1)- 其他值:找到的位置索引
第二步:找到占位符的情况
if (pos != std::string::npos)
{
// 情况A:正常替换流程
}
详细执行流程:
2.2.1 输出占位符前的文本
ss << format.substr(0, pos); // 输出{}之前的部分
format.substr(0, pos):提取从开始到{}之前的所有字符- 这些是普通文本,直接输出到字符串流
2.2.2 输出当前参数值
ss << value; // 输出参数值
- 这是类型安全的关键:使用
operator<<自动处理各种类型 - 支持所有重载了
<<操作符的类型(int、double、string、自定义类型等)
2.2.3 递归处理剩余部分
format_string_impl(ss, format.substr(pos + 2), std::forward<Args>(args)...);
参数分解:
ss:继续使用同一个字符串流(累积结果)format.substr(pos + 2):跳过{}(2个字符),取剩余格式字符串std::forward<Args>(args)...:完美转发剩余参数包
第三步:未找到占位符的情况(参数过多)
else
{
// 情况B:参数比占位符多
}
详细执行流程:
2.3.1 输出剩余格式字符串
ss << format; // 没有{}了,直接输出整个剩余字符串
2.3.2 处理多余参数
if constexpr (sizeof...(args) >= 0) // 编译时检查,总是true
{
ss << " [EXTRA_ARGS: " << value; // 标记并输出第一个多余参数
// 使用折叠表达式输出剩余多余参数
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...); // C++17折叠表达式
}
ss << "]";
}
3. 完整执行流程示例
示例1:正常情况
// 调用:format_string_impl(ss, "Hello {}! The answer is {}", "World", 42)
// 执行流程:
// 第一次调用:T=const char*, Args=int
pos = "Hello {}! The answer is {}".find("{}") → 6
输出: "Hello " // substr(0, 6)
输出: "World" // value
递归: format_string_impl(ss, "! The answer is {}", 42)
// 第二次调用:T=int, Args=空
pos = "! The answer is {}".find("{}") → 15
输出: "! The answer is " // substr(0, 15)
输出: 42 // value
递归: format_string_impl(ss, "")
// 第三次调用:基础情况
输出: "" // 空字符串
最终结果: "Hello World! The answer is 42"
示例2:参数过多
// 调用:format_string_impl(ss, "Hello {}", "World", 42, 3.14)
// 执行流程:
// 第一次调用:T=const char*, Args=int, double
pos = "Hello {}".find("{}") → 6
输出: "Hello " // substr(0, 6)
输出: "World" // value
递归: format_string_impl(ss, "", 42, 3.14)
// 第二次调用:T=int, Args=double
pos = "".find("{}") → std::string::npos (未找到)
进入else分支:
输出: "" // 空字符串
输出: " [EXTRA_ARGS: 42 3.14]" // 标记多余参数
最终结果: "Hello World [EXTRA_ARGS: 42 3.14]"
4. 关键技术点解析
4.1 模板递归展开
// 编译时递归展开示例:
format_string_impl(ss, "A{}B{}C", 1, 2)
→ 展开为:
ss << "A";
ss << 1;
ss << "B";
ss << 2;
ss << "C";
4.2 完美转发
std::forward<Args>(args)... // 保持参数的值类别(左值/右值)
作用:
- 避免不必要的拷贝
- 保持移动语义
4.3 编译时条件判断
if constexpr (sizeof...(args) > 0)
{
((ss << " " << args), ...); // 折叠表达式
}
优势:
if constexpr:编译时判断,不满足条件的代码不会生成- 折叠表达式:简洁的参数包展开语法
5. 设计优势
5.1 类型安全
// 编译时类型检查,避免运行时错误
format_string_impl(ss, "Value: {}", some_object); // 需要some_object支持<<操作符
5.2 灵活性
// 支持任意类型和数量
format_string_impl(ss, "{} + {} = {}", 2, 3, 5); // 整数
format_string_impl(ss, "PI: {:.2f}", 3.14159); // 浮点数
format_string_impl(ss, "Hello {}!", std::string("World")); // 字符串
5.3 错误处理
// 参数不足:保留{}
format_string_impl(ss, "Hello {}!", 123, 456)
→ "Hello 123! [EXTRA_ARGS: 456]"
// 占位符过多:正常输出剩余{}
format_string_impl(ss, "Hello {}! {}", "World")
→ "Hello World! {}"
6. 潜在改进点
6.1 性能优化
// 当前:每次递归都创建子字符串
format.substr(pos + 2) // 可能产生临时字符串
// 优化:使用string_view避免拷贝
format_string_impl(ss, std::string_view(format).substr(pos + 2), ...)
6.2 功能增强
// 支持格式说明符(如{:.2f})
// 支持位置参数(如{1}、{0})
// 支持命名参数
7. 总结
这个格式化函数的设计体现了现代C++模板元编程的核心:
- 递归模板:编译时展开参数包
- 类型安全:利用C++重载机制自动处理不同类型
- 错误恢复:对参数不匹配情况给出明确提示
- 零开销抽象:大部分工作在编译期完成
虽然功能上不如成熟的格式化库(如fmtlib)完善,但作为日志系统的核心组件,它在简洁性、性能和类型安全之间取得了很好的平衡。
flush_all()函数的设计和作用。
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush();
}
// 也可以考虑刷新控制台输出
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
// 控制台输出通常是行缓冲的,但可以强制刷新
std::cout << std::flush;
}
}
}
1. 函数的核心作用
1.1 数据持久化保证
// 主要目的:确保所有缓冲的日志数据真正写入到物理设备
file_target->flush(); // 文件:将OS缓冲区数据写入磁盘
std::cout << std::flush; // 控制台:刷新输出缓冲区
为什么需要刷新?
- 文件缓冲:默认情况下,文件写入会先进入OS缓冲区,稍后才真正写入磁盘
- 控制台缓冲:控制台输出通常是行缓冲的,但程序崩溃时可能丢失数据
- 数据安全:确保关键日志不会因为程序崩溃或断电而丢失
1.2 典型使用场景
// 场景1:程序正常退出前
Logger::getInstance()->flush_all();
// 场景2:关键操作完成后
performCriticalOperation();
Logger::getInstance()->flush_all(); // 确保操作日志已保存
// 场景3:程序崩溃前(信号处理函数中)
void signal_handler(int sig) {
Logger::getInstance()->flush_all();
exit(sig);
}
2. 函数执行逻辑分解
2.1 锁保护阶段
std::shared_lock lock(targets_mutex);
执行逻辑:
- 获取读锁(共享锁),允许多个线程同时调用
flush_all() - 保护
outputTargets容器在遍历过程中不被修改 - 如果此时有线程正在
addOutputTarget(),会等待写锁释放
2.2 目标遍历与类型识别
for (auto &target : outputTargets)
{
// 对每个输出目标进行类型检查和相应处理
}
2.2.1 文件目标识别与刷新
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
{
file_target->flush(); // 文件目标强制刷新
}
详细执行:
target.get():从unique_ptr获取原始指针dynamic_cast<File_OutputTarget *>:运行时类型识别- 成功:返回有效的指针,进入if分支
- 失败:返回
nullptr,继续后续检查
file_target->flush():调用文件目标的刷新方法
文件刷新的底层作用:
// 在File_OutputTarget内部可能类似:
void File_OutputTarget::flush() {
if (file_stream.is_open()) {
file_stream.flush(); // C++流刷新
// 或者:fsync(fileno(file_stream)); // 系统调用,强制写入磁盘
}
}
2.2.2 控制台目标识别与刷新
else if (auto console_target = dynamic_cast<Console_OutputTarget *>(target.get()))
{
std::cout << std::flush; // 控制台刷新
}
详细执行:
- 如果文件类型转换失败,尝试转换为控制台类型
std::cout << std::flush:刷新标准输出流缓冲区
控制台刷新的必要性:
// 没有刷新的情况:
std::cout << "Important log message"; // 可能还在缓冲区
// 程序崩溃 → 消息丢失
// 有刷新的情况:
std::cout << "Important log message" << std::flush; // 立即显示
// 程序崩溃 → 消息已输出
2.2.3 其他目标类型
// 注意:这里只处理了文件和控制台目标
// 其他类型的目标(如网络输出、数据库输出等)会被忽略
// 这是设计上的选择,可能需要根据需求扩展
3. 设计决策分析
3.1 为什么使用shared_lock而不是unique_lock?
// 正确:使用共享锁
std::shared_lock lock(targets_mutex); // 允许多个flush操作并发
// 如果错误使用排他锁:
std::unique_lock lock(targets_mutex); // 不必要地串行化flush操作
并发场景分析:
// 线程A:flush_all() - 获取shared_lock ✓
// 线程B:flush_all() - 获取shared_lock ✓ (允许并发)
// 线程C:log() - 获取shared_lock ✓ (允许并发输出)
// 线程D:addOutputTarget() - 等待unique_lock (写操作互斥)
3.2 为什么使用dynamic_cast?
// 方案A:dynamic_cast(当前实现)
if (auto file_target = dynamic_cast<File_OutputTarget *>(target.get()))
// 方案B:虚函数(替代方案)
class LogOutputTarget {
public:
virtual void flush() = 0; // 强制所有子类实现flush
};
设计权衡:
dynamic_cast优势:不需要修改基类接口,对不支持flush的目标更友好- 虚函数优势:统一的接口,类型安全,但需要所有目标都实现flush
3.3 为什么分开处理不同类型?
// 当前:针对不同类型采用不同刷新策略
文件 → file_target->flush() // 文件系统刷新
控制台 → std::cout << std::flush // 标准流刷新
网络 → 无操作 // 可能不需要或不同机制
合理性:
- 不同输出目标有不同的刷新语义
- 文件需要确保数据持久化到磁盘
- 控制台需要确保用户立即看到输出
- 网络目标可能依赖TCP的可靠性保证
4. 实际执行流程示例
示例1:混合输出目标
// 假设outputTargets包含:
// [0]: File_OutputTarget (日志文件)
// [1]: Console_OutputTarget (控制台)
// [2]: Network_OutputTarget (网络,当前不处理)
void flush_all()
{
std::shared_lock lock(targets_mutex); // 获取读锁
// 遍历目标0:File_OutputTarget
auto file_target = dynamic_cast<File_OutputTarget*>(targets[0].get());
// file_target != nullptr → 执行file_target->flush()
// 遍历目标1:Console_OutputTarget
auto console_target = dynamic_cast<Console_OutputTarget*>(targets[1].get());
// console_target != nullptr → 执行std::cout << std::flush
// 遍历目标2:Network_OutputTarget
auto network_target = dynamic_cast<Network_OutputTarget*>(targets[2].get());
// network_target == nullptr → 无操作
// 注意:即使有Network_OutputTarget类型,当前代码也不会处理
}
示例2:并发刷新场景
// 线程1:flush_all() - 开始执行
std::shared_lock lock(targets_mutex); // 获取读锁成功
// 线程2:flush_all() - 同时执行
std::shared_lock lock(targets_mutex); // 获取读锁成功(允许并发)
// 线程3:addOutputTarget() - 尝试添加新目标
std::unique_lock lock(targets_mutex); // 等待读锁释放(阻塞)
// 线程1和线程2并发刷新各自的目标
// 完成后释放读锁
// 线程3获得写锁,添加新目标
5. 潜在改进建议
5.1 扩展其他目标类型
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
if (auto file_target = dynamic_cast<File_OutputTarget*>(target.get()))
{
file_target->flush();
}
else if (auto console_target = dynamic_cast<Console_OutputTarget*>(target.get()))
{
std::cout << std::flush;
}
else if (auto network_target = dynamic_cast<Network_OutputTarget*>(target.get()))
{
network_target->flush_socket(); // 网络刷新
}
// 可以继续添加其他类型...
}
}
5.2 使用虚函数统一接口
class LogOutputTarget {
public:
virtual ~LogOutputTarget() = default;
virtual void write(const std::string& message) = 0;
virtual void flush() {} // 默认空实现,子类可选重写
};
class File_OutputTarget : public LogOutputTarget {
public:
void flush() override {
file_stream.flush();
// 可能的fsync调用
}
};
// 这样flush_all可以简化为:
void flush_all() {
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets) {
target->flush(); // 统一接口调用
}
}
5.3 错误处理增强
void flush_all()
{
std::shared_lock lock(targets_mutex);
for (auto &target : outputTargets)
{
try {
if (auto file_target = dynamic_cast<File_OutputTarget*>(target.get()))
{
file_target->flush();
}
else if (auto console_target = dynamic_cast<Console_OutputTarget*>(target.get()))
{
std::cout << std::flush;
}
} catch (const std::exception& e) {
// 记录刷新失败,但不影响其他目标
std::cerr << "Flush failed: " << e.what() << std::endl;
}
}
}
6. 总结
这个flush_all()函数是一个关键的数据安全机制,它的设计体现了:
- 线程安全:使用读写锁保护共享资源
- 类型感知:针对不同输出目标采用合适的刷新策略
- 性能考虑:允许并发刷新操作
- 务实设计:只处理确实需要刷新的目标类型
在重要的生产环境中,定期或在关键节点调用flush_all()可以显著提高日志的可靠性,确保在异常情况下不会丢失关键调试信息。
-正式版&spm=1001.2101.3001.5002&articleId=155196810&d=1&t=3&u=dd381df274a94118a7e3965a3a7ceb5a)
4785

被折叠的 条评论
为什么被折叠?



