这是一个轻量级异步日志器(async logger)。目标是用现代 C++(线程、互斥、条件变量)实现一个线程安全、低阻塞的日志写入方案:生产者将格式化后的日志消息推入队列,后台消费者线程异步写入文件并可选地输出到终端。整体实现包含一个简单的占位符格式化(暂用{},可自行修改)、类型到字符串的通用转换和优雅的后台线程关闭机制。
hpp
#ifndef MY_LOG
#define MY_LOG
#include <iostream>
#include <queue>
#include <mutex>
#include <string>
#include <condition_variable>
#include <thread>
#include <fstream>
#include <atomic>
#include <sstream>
#include <vector>
#include <stdexcept>
/**
* @brief 将单个参数转换为字符串的辅助函数。
*
* 使用 std::ostringstream 将任意支持 operator<< 的类型转换为 std::string。
*
* @tparam T 参数类型(可以是右值引用或左值引用类型)。
* @param arg 要转换的参数,必须可通过 operator<< 输出到流。
* @return std::string 参数的字符串表示。
*/
template <typename T>
std::string my_to_string(T &&arg)
{
std::ostringstream oss;
// 如果T为自定义类型,需要实现<<的重载
oss << std::forward<T>(arg);
return oss.str();
}
/**
* @brief 线程安全的日志消息队列。
*
* 该类提供生产者-消费者模式的简易实现:多个生产者可以调用 push()
* 将日志消息放入队列,单个或多个消费者通过 pop() 获取消息。通过 shutdown()
* 可以通知所有等待的线程退出,用于优雅关闭后台线程。
*/
class LogQueue
{
private:
std::queue<std::string> _queue;
std::condition_variable _cv;
std::mutex _mtx;
bool _isshut = false;
public:
/**
* @brief 将一条日志消息加入队列。
*
* 线程安全,内部会在队列从空变为非空时通知一个等待线程。
*
* @param msg 要加入的日志消息(按值或按引用传入会被拷贝到队列中)。
*/
void push(const std::string &msg)
{
std::lock_guard<std::mutex> lock(_mtx);
_queue.push(msg);
if (_queue.size() == 1)
{
_cv.notify_one();
}
}
/**
* @brief 从队列中弹出一条消息(阻塞直到有消息或队列被关闭)。
*
* @param msg 输出参数:成功弹出时存放消息。
* @return true 成功弹出并返回消息。
* @return false 队列已关闭且为空,用于通知消费者退出循环。
*/
bool pop(std::string &msg)
{
std::unique_lock<std::mutex> lock(_mtx);
// 防止虚假唤醒,需要判断队列是否为空再继续
// 线程挂起会解锁
_cv.wait(lock, [this]() -> bool
{ return !_queue.empty() || _isshut; });
// 消费逻辑
if (_queue.empty() && _isshut)
return false;
msg = _queue.front();
_queue.pop();
return true;
}
/**
* @brief 关闭队列并通知所有阻塞的等待者。
*
* 该操作会将内部状态标记为关闭,并唤醒所有在 pop() 上等待的线程。
*/
void shutdown()
{
std::lock_guard<std::mutex> lock(_mtx);
_isshut = true;
_cv.notify_all();
}
public:
/**
* @brief 构造函数,初始化队列。
*/
LogQueue() = default;
/**
* @brief 析构函数。
*/
~LogQueue() = default;
};
/**
* @brief 异步日志器类。
*
* Logger 在构造时打开指定的日志文件并启动一个后台线程,从内部的 LogQueue 中
* 异步消费日志消息并写入文件;可选地同时输出到控制台。析构时会优雅关闭后台线程并关闭文件。
*/
class Logger
{
public:
/**
* @brief 构造并启动 Logger。
*
* 打开日志文件并启动一个后台线程用于异步写入。
*
* @param filename 日志文件路径(如果文件不存在会创建)。
* @param coutopen 如果为 true,则在写入文件的同时将日志输出到 std::cout(线程安全)。
* @throws std::runtime_error 无法打开日志文件时抛出异常。
*/
Logger(const std::string &filename, bool coutopen) : _logfile(filename, std::ios::out | std::ios::app), _exitflag(false), _console(coutopen)
{
if (!_logfile.is_open())
{
throw std::runtime_error("failed to open log file.");
}
_workerthread = std::thread([this]()
{
std::string msg;
while (this->_logqueue.pop(msg)){
if (this->_logfile.is_open()){
_logfile << msg << std::endl;
}
if (_console){
std::lock_guard<std::mutex> lock(_cout_mtx);
std::cout << msg << std::endl;
}
} });
}
/**
* @brief 构造函数(不输出到控制台)。
*
* 等同于 Logger(filename, false)。
*
* @param filename 日志文件路径。
* @throws std::runtime_error 无法打开日志文件时抛出异常。
*/
Logger(const std::string &filename) : _logfile(filename, std::ios::out | std::ios::app), _exitflag(false), _console(false)
{
if (!_logfile.is_open())
{
throw std::runtime_error("failed to open log file.");
}
_workerthread = std::thread([this]()
{
std::string msg;
while (this->_logqueue.pop(msg)){
if (this->_logfile.is_open()){
_logfile << msg << std::endl;
}
if (_console){
std::lock_guard<std::mutex> lock(_cout_mtx);
std::cout << msg << std::endl;
}
} });
}
/**
* @brief 析构函数,停止后台线程并关闭文件。
*
* 将标志设置为退出状态、调用 LogQueue::shutdown() 通知后台线程,并 join 线程。
*/
~Logger()
{
_exitflag = true;
_logqueue.shutdown();
if (_workerthread.joinable())
{
_workerthread.join();
}
if (_logfile.is_open())
{
_logfile.close();
}
};
public:
/**
* @brief 将格式化消息异步写入日志。
*
* 使用 formatMessage() 将 format 中的 {} 占位符替换为后续参数的字符串表示,
* 然后将生成的消息推入队列,由后台线程异步写入文件(以及可选的控制台)。
*
* @tparam Args 参数类型列表。
* @param format 带有 {} 占位符的格式字符串。
* @param args 与占位符对应的参数列表。
*/
template <typename... Args>
void log(const std::string &format, Args &&...args)
{
_logqueue.push(formatMessage(format, std::forward<Args>(args)...));
}
private:
/**
* @brief 将格式字符串中的 {} 依次替换为传入参数的字符串表示。
*
* 简单实现,不支持索引、宽度或其他格式化选项;当参数少于占位符时保留 "{}" 原样,
* 当参数多于占位符时多余参数被忽略。
*
* @tparam Args 参数类型列表。
* @param format 包含 {} 占位符的格式字符串。
* @param args 参数列表。
* @return std::string 替换后的最终字符串。
*/
template <typename... Args>
std::string formatMessage(const std::string &format, Args &&...args)
{
std::vector<std::string> arg_string = {my_to_string(std::forward<Args>(args))...};
std::ostringstream oss;
size_t arg_index = 0;
// 遍历格式字符串,替换{}为参数字符串
for (size_t i = 0; i < format.size(); ++i)
{
if (format[i] == '{' && i + 1 < format.size() && format[i + 1] == '}')
{
// 如果还有参数,替换为参数字符串,否则保留原样
if (arg_index < arg_string.size())
{
oss << arg_string[arg_index];
++arg_index;
}
else
{
oss << "{}";
}
++i; // 跳过下一个字符
}
else
{
oss << format[i];
}
}
return oss.str();
}
private:
LogQueue _logqueue;
std::thread _workerthread;
std::ofstream _logfile;
std::atomic<bool> _exitflag;
std::mutex _cout_mtx; // 保护 std::cout 的互斥锁
bool _console = false; // 是否同时输出到控制台
};
#endif
使用方法
这是一个head-only的库,只需要简单地包含头文件即可以调用,下面是使用示例
main.cpp
#include "log.hpp"
#include <chrono>
#include <thread>
int main() {
try {
Logger logger("example.log", true);
logger.log("hello {}, {}", "world", 2026);
logger.log("another line: {} - {}", "test", 3.14);
logger.log("this is a log message without parameters");
logger.log("logging with more parameters: {}, {}, {}", "param1", 42, 3.14);
logger.log("logging with fewer parameters: {}, {}", "only one");
} catch (const std::exception &e) {
std::cerr << "logger init failed: " << e.what() << std::endl;
return 1;
}
return 0;
}
使用效果(同时开启终端输出与文件输出时)
文件输出:
终端输出:
后续可以考虑增加不同输出等级限制,比如【Info】 【Warn】 【Error】这些