同步/异步日志系统
同步/异步日志系统
- 项目演示
- 基础测试
- 性能测试
- 测试环境:
- 同步日志器单线程
- 同步日志器多线程
- 异步日志器单线程
- 异步日志器多线程
- 工具类(util.hpp)
- 日志等级
- level.hpp
- 日志消息
- message.hpp
- 日志消息格式化
- formatter.hpp
- 日志消息落地
- sink.hpp
- 日志器
- 异步线程
- buffer.hpp(缓冲区设计)
- looper.hpp(异步线程模块)
- 异步日志器模块(在logger.hpp中的一个类)
- 难点(未实现):实现双向循环队列缓冲区时数据到达上限扩不扩容?
- 项目中的做法:
- 日志管理(单例)
- 日志管理器模块(在logger.hpp中的一个类)
- 提供全局接口&宏函数,方便用户使用
源码地址:点这里
项目演示
基础测试
打印20w条日志分别到标准输出、普通文件、滚动文件(每个文件大小限制为1MB)中
标准输出:

标准文件:

滚动文件:

性能测试
测试环境:
Ubuntu云服务器

同步日志器单线程

同步日志器多线程

异步日志器单线程

异步日志器多线程

工具类(util.hpp)
#pragma once
/*
工具类:1.获取系统时间(now)2.判断文件是否存在(exists)3.获取文件所在路径(path)4.创建目录(createdirectory)函数都是静态成员函数,方便直接使用,不用实例化对象util:工具
*/
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <cassert>
#include <sys/stat.h>
#include <filesystem>
using namespace std;namespace hsl_log
{namespace util{class date{public:static size_t now() { return (size_t)time(nullptr); }};class file{public:static bool exists(const std::string &name){struct stat st;return stat(name.c_str(), &st) == 0;}static std::string path(const std::string &name){if (name.empty())return ".";size_t pos = name.find_last_of("/\\");if (pos == std::string::npos)return ".";return name.substr(0, pos + 1);}static void create_directory_1(const std::string &path){if (path.empty())return;if (exists(path))return;size_t pos, idx = 0;while (idx < path.size()){pos = path.find_first_of("/\\", idx);if (pos == std::string::npos){mkdir(path.c_str(), 0755);return;}if (pos == idx){idx = pos + 1;continue;}std::string subdir = path.substr(0, pos);if (subdir == "." || subdir == ".."){idx = pos + 1;continue;}if (exists(subdir)){idx = pos + 1;continue;}mkdir(subdir.c_str(), 0755);idx = pos + 1;}}static void create_directory(const string &pathname){filesystem::create_directories(pathname);}};}
}
日志等级

level.hpp
#pragma once
/*
枚举类:1.定义枚举类,枚举出日志等级2.提供转换接口,将枚举转换为对应字符串level:级别UNKNOW:未知的
*/namespace hsl_log{
class LogLevel{public:enum class value {UNKNOW = 0,DEBUG,INFO,WARN,ERROR,FATAL,OFF};static const char *toString(LogLevel::value l) {switch(l) {case LogLevel::value::DEBUG: return "DEBUG";case LogLevel::value::INFO: return "INFO";case LogLevel::value::WARN: return "WARN";case LogLevel::value::ERROR: return "ERROR";case LogLevel::value::FATAL: return "FATAL";case LogLevel::value::OFF: return"OFF";}return "UNKNOW";}
};
}
日志消息

message.hpp
#pragma once
/*
日志消息类:1.日志输出时间2.日志等级3.源文件名称4.源代码行号5.线程ID6.日志主体消息7.日志器名称
*/#include <iostream>
#include <thread>
#include "level.hpp"
#include "util.hpp"
using namespace std;namespace hsl_log
{struct LogMsg{using ptr = std::shared_ptr<LogMsg>;size_t _line; // 行号size_t _ctime; // 时间std::thread::id _tid; // 线程IDstd::string _name; // 日志器名称std::string _file; // 文件名std::string _payload; // 日志消息LogLevel::value _level; // 日志等级LogMsg(std::string &name, std::string file, size_t line, std::string &&payload,LogLevel::value level) : _name(name), _file(file), _payload(std::move(payload)), _level(level),_line(line), _ctime(util::date::now()), _tid(std::this_thread::get_id()){// std::cout << "构造msg\n";}//~LogMsg() { /*std::cout << "析构msg\n";*/}};
}
日志消息格式化

formatter.hpp
#pragma once
/*
消息格式化类:formatter:格式化Item:项(子项)1.抽象一个格式化基类2.基于基类,派生出不同的格式化子项子类子项:{日志输出时间日志等级源文件名称源代码行号线程ID日志主体消息日志器名称换行制表符其他}这样就可以在父类中定义父类指针的数组,指向不同的格式化子类对象(多态!)
*/#include "util.hpp"
#include "message.hpp"
#include "level.hpp"
#include <memory>
#include <vector>
#include <tuple>
#include <sstream>namespace hsl_log
{// 抽象一个"格式化项"基类class FormatItem{public:virtual ~FormatItem() {}// 等价于typedef std::shared_ptr<FormatItem> ptr;using ptr = shared_ptr<FormatItem>; // 别名virtual void format(ostream &out, LogMsg &msg) = 0;};/*基于基类,派生出不同的格式化子项子类子项:{日志输出时间日志等级源文件名称源代码行号线程ID日志主体消息日志器名称换行制表符其他}*/// 消息主体class MsgFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << msg._payload;}};// 等级class LevelFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << LogLevel::toString(msg._level);}};// 时间class TimeFormatItem : public FormatItem{public:TimeFormatItem(const string &fmt = "%H:%M:%S"): _time_fmt(fmt){// if (fmt.empty()) _time_fmt = "%H:%M:%S";}/*void format(ostream &out, LogMsg &msg) override{time_t t = msg._ctime;struct tm lt;localtime_r(&t, <);char tmp[128];strftime(tmp, 127, _time_fmt.c_str(), <);out << tmp;}*/void format(ostream &out, LogMsg &msg) override{string tmp = TimeStampExLocalTime();out << tmp.c_str();}static std::string TimeStampExLocalTime(){time_t currtime = time(nullptr);struct tm *curr = localtime(&currtime);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,curr->tm_hour, curr->tm_min, curr->tm_sec);return time_buffer;}private:string _time_fmt; //%H:%M:%S};// 行号class LineFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << msg._line;}};// 源文件名class FileFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << msg._file;}};// 线程IDclass ThreadFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << msg._tid;}};// 日志器名称class LoggerFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << msg._name;}};// 制表符class TabFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << '\t';}};// 换行符class NLineFormatItem : public FormatItem{public:void format(ostream &out, LogMsg &msg) override{out << '\n';}};// 其他class OtherFormatItem : public FormatItem{public:OtherFormatItem(const string &str) : _str(str) {}void format(ostream &out, LogMsg &msg) override{out << _str;}private:string _str;};// 格式化类(主模块)/*%d 日期%T 缩进%t 线程id%p 日志级别%c 日志器名称%f 文件名%l 行号%m 日志消息%n 换行*/class Formatter{public:using ptr = shared_ptr<Formatter>;Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l]%T%m%n") : _pattern(pattern){assert(parsePattern());}// 格式化到标准输出void format(ostream &out, LogMsg &msg){for (auto &items : _items){items->format(out, msg);}}// 能获取到格式化后的字符串string format(LogMsg &msg){stringstream ss;format(ss, msg);return ss.str();}private:// 解析字符串bool parsePattern(){// 1.对格式化规则进行解析vector<pair<string, string>> fmt_order;//存放key,val,方便最后存进成员变量_items里size_t pos = 0; //string key, val; // 分别存放 %x中的x and 其他字符(原始字符)while (pos < _pattern.size()){// 1.1处理字符串--判断是否为 %,否则就是其他字符if (_pattern[pos] != '%'){val.push_back(_pattern[pos++]);continue;}// 1.2到这说明pos位置就是%,还要判断一下%%的情况if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back('%');pos += 2;continue;}if (val.empty() == false){fmt_order.push_back(make_pair("", val));val.clear();}// 1.3到这说明一定是格式化字符(%X)// pos位置就是%,pos+1位置就是我们要的字符pos += 1;if (pos == _pattern.size()){cout << "%之后没有对应的格式化字符" << endl;return false;}key = _pattern[pos];pos += 1;// 1.4这时pos指向格式化字符的下一个位置if (pos < _pattern.size() && _pattern[pos] == '{') // 如果是{{pos += 1; // 指向子规则的起始位置:%while (pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos++]);}if (pos == _pattern.size()) // 没有找到'}'{cout << "子规则{}匹配出错!\n";return false;}pos += 1; // 这时候pos在‘}’位置,+1到}fmt_order.push_back(make_pair(key, val));key.clear();val.clear();}// 2.根据解析得到的数据格式化子项数组成员for (auto &it : fmt_order){_items.push_back(createItem(it.first, it.second));}return true;}// 根据不同的格式化字符创建不同的格式化子项 %x //其他字符(原始字符)FormatItem ::ptr createItem(const string &key, const string &val){if (key == "d")return make_shared<TimeFormatItem>(val);if (key == "T")return make_shared<TabFormatItem>();if (key == "t")return make_shared<ThreadFormatItem>();if (key == "p")return make_shared<LevelFormatItem>();if (key == "c")return make_shared<LoggerFormatItem>();if (key == "f")return make_shared<FileFormatItem>();if (key == "l")return make_shared<LineFormatItem>();if (key == "m")return make_shared<MsgFormatItem>();if (key == "n")return make_shared<NLineFormatItem>();if (key == "")return make_shared<OtherFormatItem>(val);cout << "没有对应的格式化字符:%" << key << endl;abort();}private:string _pattern; // 符合格式化规则的字符串vector<FileFormatItem::ptr> _items; // 定义存放格式化对象的数组};
}
日志消息落地


sink.hpp
#pragma once
/*
日志落地模块实现1. 抽象落地基类2. 派生落地子类(根据不同的方向进行派生)3. 使用工厂模式进行创建与表示的分离
sink:下沉/落地
*/
#include "util.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include <memory>
#include <mutex>namespace hsl_log
{// 日志落地基类,定义统一的日志输出接口 class LogSink{public:using ptr = std::shared_ptr<LogSink>;//智能指针的别名(==typedef...)LogSink() {}virtual ~LogSink() {}virtual void log(const char *data, size_t len) = 0;// 纯虚函数,定义日志输出接口};// 标准输出class StdoutSink : public LogSink{public:using ptr = std::shared_ptr<StdoutSink>;//StdoutSink() = default;// 默认构造函数void log(const char *data, size_t len){std::cout.write(data, len);}};// 指定文件class FileSink : public LogSink{public:using ptr = std::shared_ptr<FileSink>;FileSink(const std::string &filename) : _filename(filename){// 1.创建日志所在的目录util::file::create_directory(util::file::path(filename));// 2.创建并打开日志文件(app追加形式)_ofs.open(_filename, std::ios::binary | std::ios::app);assert(_ofs.is_open());}const std::string &file() { return _filename; }void log(const char *data, size_t len){_ofs.write((const char *)data, len);if (_ofs.good() == false){std::cout << "日志输出文件失败!\n";}}private:std::string _filename;std::ofstream _ofs;};//滚动文件class RollSink : public LogSink{public:using ptr = std::shared_ptr<RollSink>;RollSink(const std::string &basename, size_t max_fsize) : _basename(basename), _max_fsize(max_fsize), _cur_fsize(0),_name_count(0){util::file::create_directory(util::file::path(basename));}void log(const char *data, size_t len){initLogFile();_ofs.write(data, len);if (_ofs.good() == false){std::cout << "日志输出文件失败!\n";}_cur_fsize += len;}private:// 初始化日志文件,检查是否需要创建新文件void initLogFile(){// 如果文件未打开或当前文件大小超过限制,则创建新文件if (_ofs.is_open() == false || _cur_fsize >= _max_fsize){_ofs.close();std::string name = createFilename();_ofs.open(name, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize = 0;return;}return;}//用时间戳当文件名,保证文件名的唯一性std::string createFilename(){time_t t = time(NULL);struct tm lt;localtime_r(&t, <);std::stringstream ss;ss << _basename;ss << lt.tm_year + 1900;ss << lt.tm_mon + 1;ss << lt.tm_mday;ss << lt.tm_hour;ss << lt.tm_min;ss << lt.tm_sec;ss << "-";ss << _name_count++;ss << ".log";return ss.str();}private:size_t _name_count;std::string _basename;std::ofstream _ofs;size_t _max_fsize;size_t _cur_fsize;};// 日志落地工厂类,用于创建各种类型的日志落地器class SinkFactory{public:// 工厂方法,创建指定类型的日志落地器// 模板参数:// SinkType: 要创建的落地器类型// Args: 构造参数类型// 参数:// args: 传递给构造函数的参数// 返回值: 落地器的共享指针template <typename SinkType, typename... Args>static LogSink::ptr create(Args &&...args){//使用完美转发创建指定类型的落地器return std::make_shared<SinkType>(std::forward<Args>(args)...);}};}
日志器



#pragma once
/*
日志器模块:1. 抽象日志基类2. 派生出不同的子类(同布日志器类 & 异步日志器类)*/
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <vector>
#include <list>
#include <atomic>
#include <unordered_map>
#include <cstdarg>
#include <type_traits>namespace hsl_log
{class SyncLogger;class AsyncLogger;class Logger{public:enum class Type{LOGGER_SYNC = 0,LOGGER_ASYNC};using ptr = std::shared_ptr<Logger>;Logger(const std::string &name,Formatter::ptr formatter,std::vector<LogSink::ptr> &sinks,LogLevel::value level = LogLevel::value::DEBUG) : _name(name), _level(level), _formatter(formatter),_sinks(sinks.begin(), sinks.end()){}std::string loggerName() { return _name; }LogLevel::value loggerLevel() { return _level; }void debug(const char *file, size_t line, const char *fmt, ...){// 1.判断是否达到了日志等级if (shouldLog(LogLevel::value::DEBUG) == false){return;}// 处理不定参...va_list al;va_start(al, fmt);log(LogLevel::value::DEBUG, file, line, fmt, al);va_end(al);}void info(const char *file, size_t line, const char *fmt, ...){if (shouldLog(LogLevel::value::INFO) == false)return;va_list al;va_start(al, fmt);log(LogLevel::value::INFO, file, line, fmt, al);va_end(al);}void warn(const char *file, size_t line, const char *fmt, ...){if (shouldLog(LogLevel::value::WARN) == false)return;va_list al;va_start(al, fmt);log(LogLevel::value::WARN, file, line, fmt, al);va_end(al);}void error(const char *file, size_t line, const char *fmt, ...){if (shouldLog(LogLevel::value::ERROR) == false)return;va_list al;va_start(al, fmt);log(LogLevel::value::ERROR, file, line, fmt, al);va_end(al);}void fatal(const char *file, size_t line, const char *fmt, ...){if (shouldLog(LogLevel::value::FATAL) == false)return;va_list al;va_start(al, fmt);log(LogLevel::value::FATAL, file, line, fmt, al);va_end(al);}public:// 建造者模式:就是为了解决一个对象构造过于复杂,参数过多的问题,为用户使用提供便利 降低用户使用的复杂度// 1.抽象一个日志建造者类// 1.1 设置日志器类型// 1.2 将不同类型日志器的创建放到同一个日志建造者类中完成class Builder{public:using ptr = std::shared_ptr<Builder>;Builder() : _level(LogLevel::value::DEBUG),_logger_type(Logger::Type::LOGGER_SYNC),_looper_type(AsyncType::ASYNC_SAFE) {}void buildLoggerName(const std::string &name) { _logger_name = name; }void buildLoggerLevel(LogLevel::value level) { _level = level; }void buildEnableUnSafeAsync() {_looper_type = AsyncType::ASYNC_UNSAFE; }void buildLoggerType(Logger::Type type) { _logger_type = type; }void buildFormatter(const std::string pattern) { _formatter = std::make_shared<Formatter>(pattern); }void buildFormatter(const Formatter::ptr &formatter) { _formatter = formatter; }//添加日志落地目标template <typename SinkType, typename... Args>void buildSink(Args &&...args){auto sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(sink);}// 纯虚函数,由具体建造者实现virtual Logger::ptr build() = 0;protected:AsyncType _looper_type;Logger::Type _logger_type;std::string _logger_name;LogLevel::value _level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};protected:bool shouldLog(LogLevel::value level) { return level >= _level; }// log序列化void log(LogLevel::value level, const char *file, size_t line, const char *fmt, va_list al){// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串char *buf;std::string msg;int len = vasprintf(&buf, fmt, al);if (len < 0){msg = "格式化日志消息失败!!";}else{msg.assign(buf, len);free(buf);}// 3. 构造LogMsg对象// LogMsg(name, file, line, payload, level)LogMsg lm(_name, file, line, std::move(msg), level);// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串std::stringstream ss;_formatter->format(ss, lm);logIt(std::move(ss.str()));}virtual void logIt(const std::string &msg) = 0;protected:std::mutex _mutex;std::string _name;Formatter::ptr _formatter; // 格式化std::atomic<LogLevel::value> _level; // 限制等级std::vector<LogSink::ptr> _sinks; // 落地方向};// 同步日志class SyncLogger : public Logger{public:using ptr = std::shared_ptr<SyncLogger>;SyncLogger(const std::string &name,Formatter::ptr formatter,std::vector<LogSink::ptr> &sinks,LogLevel::value level = LogLevel::value::DEBUG) : Logger(name, formatter, sinks, level){std::cout << LogLevel::toString(level) << " 同步日志器: " << name << "创建成功...\n";}private:void logIt(const std::string &msg) override{std::unique_lock<std::mutex> lock(_mutex);if (_sinks.empty()){return;}for (auto &it : _sinks){it->log(msg.c_str(), msg.size());}}};// 异步日志器
class AsyncLogger : public Logger{public:using ptr = std::shared_ptr<AsyncLogger>;AsyncLogger(const std::string &name,Formatter::ptr formatter,std::vector<LogSink::ptr> &sinks,LogLevel::value level = LogLevel::value::DEBUG,AsyncType looper_type= AsyncType::ASYNC_SAFE) :Logger(name, formatter, sinks, level),_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::backendLogIt, this, std::placeholders::_1),looper_type)){std::cout << LogLevel::toString(level) << "异步日志器: " << name << "创建成功...\n";}protected:void logIt(const std::string &msg) override{_looper->push(msg);}//实际落地函数,将缓冲区中的数据落地void backendLogIt(Buffer &msg){if (_sinks.empty()){return;}for (auto &it : _sinks){it->log(msg.begin(), msg.readAbleSize());}}protected:AsyncLooper::ptr _looper;};//本地日志记录器构建器(局部)class LocalLoggerBuilder : public Logger::Builder{public:Logger::ptr build() override{if (_logger_name.empty()){std::cout << "日志器名称不能为空!!";abort();}if (_formatter.get() == nullptr){std::cout << "当前日志器:" << _logger_name << " 未检测到日志格式,默认设置为[ %d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n ]!\n";_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){std::cout << "当前日志器:" << _logger_name << " 未检测到落地方向,默认设置为标准输出!\n";_sinks.push_back(std::make_shared<StdoutSink>());}Logger::ptr lp;if (_logger_type == Logger::Type::LOGGER_ASYNC){lp = std::make_shared<AsyncLogger>(_logger_name, _formatter, _sinks, _level,_looper_type);}else{lp = std::make_shared<SyncLogger>(_logger_name, _formatter, _sinks, _level);}return lp;}};//日志管理器:单例模式(懒汉)class loggerManager{private:std::mutex _mutex;Logger::ptr _root_logger;std::unordered_map<std::string, Logger::ptr> _loggers;private:loggerManager(){std::unique_ptr<LocalLoggerBuilder> slb(new LocalLoggerBuilder());slb->buildLoggerName("root");slb->buildLoggerType(Logger::Type::LOGGER_SYNC);_root_logger = slb->build();_loggers.insert(std::make_pair("root", _root_logger));}loggerManager(const loggerManager &) = delete;loggerManager &operator=(const loggerManager &) = delete;public:static loggerManager &getInstance(){static loggerManager lm;return lm;}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if (it == _loggers.end()){return false;}return true;}void addLogger(const std::string &name, const Logger::ptr logger){std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(name, logger));}Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if (it == _loggers.end()){return Logger::ptr();}return it->second;}Logger::ptr rootLogger(){std::unique_lock<std::mutex> lock(_mutex);return _root_logger;}};//全局日志建造者class GlobalLoggerBuilder : public Logger::Builder{public:Logger::ptr build() override{if (_logger_name.empty()){std::cout << "日志器名称不能为空!!";abort();}assert(loggerManager::getInstance().hasLogger(_logger_name) == false);if (_formatter.get() == nullptr){std::cout << "当前日志器:" << _logger_name << " 未检测到日志格式,默认设置为[ %d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n ]!\n";_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){std::cout << "当前日志器:" << _logger_name << " 未检测到落地方向,默认设置为标准输出!\n";_sinks.push_back(std::make_shared<StdoutSink>());}Logger::ptr lp;if (_logger_type == Logger::Type::LOGGER_ASYNC){lp = std::make_shared<AsyncLogger>(_logger_name, _formatter, _sinks, _level);}else{lp = std::make_shared<SyncLogger>(_logger_name, _formatter, _sinks, _level);}loggerManager::getInstance().addLogger(_logger_name, lp);return lp;}};}
异步线程





解决方案:双缓冲区


buffer.hpp(缓冲区设计)
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <cassert>namespace hsl_log
{//buff大小#define MAX_BUFFER_SIZE 10*1024*1024//10MB//阈值(软上限)#define THRESHOLD_BUFFER_SIZE 1024*1024*1024//1024MB//增量#define INCREMENT_BUFFER_SIZE 10*1024*1024//10MBclass Buffer{private:size_t _reader_idx;//当前可读数据的指针--本质是下标size_t _writer_idx;//当前可写数据的指针--本质是下标size_t _capacity;//缓冲区的总容量std::vector<char> _buffer;//buffer缓冲区public:// 构造函数:初始化缓冲区容量(默认10MB)Buffer(size_t capacity = MAX_BUFFER_SIZE) : _reader_idx(0), _writer_idx(0),_capacity(capacity),_buffer(_capacity) {}// 判断缓冲区是否已满(写指针是否到达末尾)bool full() { return _writer_idx == _capacity; }// 判断缓冲区是否为空(读指针是否等于写指针)bool empty() { return _reader_idx == _writer_idx; }// 获取当前可读数据的大小size_t readAbleSize() { return _writer_idx - _reader_idx; }// 获取当前可写数据的大小size_t writeAbleSize() { return _capacity - _writer_idx; }// 重置(初始化 )缓冲区(读写指针归零)void reset() { _reader_idx = _writer_idx = 0; }// 交换两个缓冲区的所有内容(高效操作)void swap(Buffer &buf){_buffer.swap(buf._buffer);std::swap(_reader_idx, buf._reader_idx);std::swap(_writer_idx, buf._writer_idx);std::swap(_capacity, buf._capacity);}// 写入数据到缓冲区void push(const char *data, size_t len){//阻塞,返回false//assert(len <= writeAbleSize());//扩容+软限制ensure_enough_size(len);//把数据拷贝进缓冲区std::copy(data, data + len, &_buffer[_writer_idx]);//将当前位置向后偏移moveWriter(len);}// 获取当前可读数据的起始地址(用于直接读取)const char *begin() { return &_buffer[_reader_idx]; }// 从缓冲区移除已读数据(推进读指针)void pop(size_t len){ moveReader(len);assert(_reader_idx <= _capacity);if (empty())reset();}private://对写指针进行向后偏移操作void moveWriter(size_t len){_writer_idx += len;}//对读指针进行向后偏移操作void moveReader(size_t len){_reader_idx += len;}//确保足够大小void ensure_enough_size(size_t len){if(len<writeAbleSize())return;size_t new_size =0;if(_buffer.size()<THRESHOLD_BUFFER_SIZE){new_size = _buffer.size()*2+len;//成倍增长}else{new_size = _buffer.size()+INCREMENT_BUFFER_SIZE+len;//线性增长//return false;//到软限制就返回false阻塞}_buffer.reserve(new_size);}};
}
looper.hpp(异步线程模块)
#pragma once
#include "util.hpp"
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include "buffer.hpp"namespace hsl_log
{enum class AsyncType{ASYNC_SAFE,//安全状态,缓冲区满了就阻塞,避免资源耗尽的风险ASYNC_UNSAFE,//非安全状态,用于性能测试};class AsyncLooper{public:using Functor = std::function<void(Buffer &buffer)>; // 定义回调函数类型using ptr = std::shared_ptr<AsyncLooper>; // 智能指针别名AsyncLooper(const Functor &cb,AsyncType looper_type = AsyncType::ASYNC_SAFE ):_looper_type(looper_type),_running(true),_looper_callback(cb),_thread(std::thread(&AsyncLooper::worker_loop, this)) // 创建工作线程{}~AsyncLooper() { stop(); } // 析构函数,自动停止处理器void stop(){_running = false; // 设置运行标志为false_pop_cond.notify_all(); // 通知所有等待的线程(唤醒所所有的消费线程)_thread.join(); // 等待工作线程结束}void push(const std::string &msg){if (_running == false)return;{std::unique_lock<std::mutex> lock(_mutex);// 等待直到缓冲区有足够空间写入新消息if(_looper_type ==AsyncType::ASYNC_SAFE)_push_cond.wait(lock, [&]{ return _tasks_push.writeAbleSize() >= msg.size(); });// 将消息写入推送缓冲区_tasks_push.push(msg.c_str(), msg.size());}// 唤醒消费者对缓冲区中的数据进行处理_pop_cond.notify_all();}private:// 线程入口函数--对消费缓冲区中的数据进行处理,处理完毕之后,初始化缓冲区,交换缓冲区void worker_loop(){while (1){// 1.判断生产缓冲区有无数据,有就交换,无就等待// “{}”给锁设置生命周期{// 加锁std::unique_lock<std::mutex> lock(_mutex);// 检查停止条件:运行标志为false且生产缓冲区为空if (_running == false && _tasks_push.empty()){return;}// 等待直到生产缓冲区有数据或处理器停止_pop_cond.wait(lock, [&]{ return !_tasks_push.empty() || !_running; });// 交换生产缓冲区和消费缓冲区_tasks_push.swap(_tasks_pop);}// 2.通知生产者线程缓冲区有空闲空间,唤醒生产者if(_looper_type ==AsyncType::ASYNC_SAFE)_push_cond.notify_all(); // 通知生产者线程缓冲区有空闲空间// 3.被唤醒后,对消费者缓冲区进行处理_looper_callback(_tasks_pop); // 调用回调函数处理消费缓冲区中的数据// 4.消息处理进行一个重置_tasks_pop.reset(); // 重置消费缓冲区}return;}private:Functor _looper_callback;private:AsyncType _looper_type;std::mutex _mutex; // 保护共享数据的互斥锁std::atomic<bool> _running; // 原子布尔值,控制循环是否继续运行std::condition_variable _push_cond; // 生产者条件变量,用于等待缓冲区空间std::condition_variable _pop_cond; // 消费者条件变量,用于等待缓冲区数据Buffer _tasks_push; // 推送/生产缓冲区(生产者写入)Buffer _tasks_pop; // 弹出/消费缓冲区(消费者读取)std::thread _thread; // 工作线程};
}

异步日志器模块(在logger.hpp中的一个类)
// 异步日志器
class AsyncLogger : public Logger{public:using ptr = std::shared_ptr<AsyncLogger>;AsyncLogger(const std::string &name,Formatter::ptr formatter,std::vector<LogSink::ptr> &sinks,LogLevel::value level = LogLevel::value::DEBUG,AsyncType looper_type= AsyncType::ASYNC_SAFE) :Logger(name, formatter, sinks, level),_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::backendLogIt, this, std::placeholders::_1),looper_type)){std::cout << LogLevel::toString(level) << "异步日志器: " << name << "创建成功...\n";}protected:void logIt(const std::string &msg) override{_looper->push(msg);}//实际落地函数,将缓冲区中的数据落地void backendLogIt(Buffer &msg){if (_sinks.empty()){return;}for (auto &it : _sinks){it->log(msg.begin(), msg.readAbleSize());}}protected:AsyncLooper::ptr _looper;};
难点(未实现):实现双向循环队列缓冲区时数据到达上限扩不扩容?
实际使用缓冲区时,无限制的扩容缓冲区是很危险的,当程序出现错误死循环时,会导致缓冲区一直刷数据,资源耗尽,程序/系统崩溃。
所以一开始想的是直接对buffer进行扩容,考虑到这一点就比较犹豫。
但如果不扩容,进行阻塞等待(返回值返回,由上层线程调用wait进行等待)的话,日志写入通常是后台操作,阻塞业务线程会降低系统整体性能。
所以到后面就想着既然不能一直扩容,也不能直接阻塞,那就两种方法结合起来用,最后采用了动态扩容 + 软上限 + 环形队列
- 做法:缓冲区满时自动扩容(如2倍增长),但设置软上限(如1024MB)。
- 软上限:达到软上限后再进行阻塞
- 环形缓冲区:覆盖最旧的日志(牺牲部分历史数据)。
项目中的做法:
缓冲区没满不管,缓冲区满时性能优先,自动扩容(如2倍增长),但设置软上限(如1024MB)。当缓冲区大小超过阈值(上限)后,采用线性增长策略
日志管理(单例)



日志管理器模块(在logger.hpp中的一个类)
//日志管理器:单例模式(懒汉)class loggerManager{private:std::mutex _mutex;Logger::ptr _root_logger;std::unordered_map<std::string, Logger::ptr> _loggers;private:loggerManager(){std::unique_ptr<LocalLoggerBuilder> slb(new LocalLoggerBuilder());slb->buildLoggerName("root");slb->buildLoggerType(Logger::Type::LOGGER_SYNC);_root_logger = slb->build();_loggers.insert(std::make_pair("root", _root_logger));}loggerManager(const loggerManager &) = delete;loggerManager &operator=(const loggerManager &) = delete;public:static loggerManager &getInstance(){static loggerManager lm;return lm;}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if (it == _loggers.end()){return false;}return true;}void addLogger(const std::string &name, const Logger::ptr logger){std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(name, logger));}Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _loggers.find(name);if (it == _loggers.end()){return Logger::ptr();}return it->second;}Logger::ptr rootLogger(){std::unique_lock<std::mutex> lock(_mutex);return _root_logger;}};
提供全局接口&宏函数,方便用户使用

#pragma once#include "logger.hpp"namespace hsl_log{//1. 提供获取指定日志器的全局接口(避免用户自己操作单例对象)Logger::ptr getLogger(const std::string &name) {return loggerManager::getInstance().getLogger(name);}Logger::ptr rootLogger() {return loggerManager::getInstance().rootLogger();}//2. 使用宏函数,对日志器的接口进行代理(代理模式)#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)//提供宏函数,直接通过默认日志器进行日志的标准打印#define LOG_DEBUG(logger, fmt, ...) (logger)->debug(fmt, ##__VA_ARGS__)#define LOG_INFO(logger, fmt, ...) (logger)->info(fmt, ##__VA_ARGS__)#define LOG_WARN(logger, fmt, ...) (logger)->warn(fmt, ##__VA_ARGS__)#define LOG_ERROR(logger, fmt, ...) (logger)->error(fmt, ##__VA_ARGS__)#define LOG_FATAL(logger, fmt, ...) (logger)->fatal(fmt, ##__VA_ARGS__)#define LOGD(fmt, ...) LOG_DEBUG(hsl_log::rootLogger(), fmt, ##__VA_ARGS__)#define LOGI(fmt, ...) LOG_INFO(hsl_log::rootLogger(), fmt, ##__VA_ARGS__)#define LOGW(fmt, ...) LOG_WARN(hsl_log::rootLogger(), fmt, ##__VA_ARGS__)#define LOGE(fmt, ...) LOG_ERROR(hsl_log::rootLogger(), fmt, ##__VA_ARGS__)#define LOGF(fmt, ...) LOG_FATAL(hsl_log::rootLogger(), fmt, ##__VA_ARGS__)}
相关文章:
同步/异步日志系统
同步/异步日志系统 项目演示基础测试性能测试测试环境:同步日志器单线程同步日志器多线程异步日志器单线程异步日志器多线程 工具类(util.hpp)日志等级level.hpp 日志消息message.hpp 日志消息格式化formatter.hpp 日志消息落地sink.hpp 日志…...
typescript html input无法输入解决办法
input里加上这个: onkeydown:(e: KeyboardEvent) > {e.stopPropagation();...
游戏引擎学习第224天
回顾游戏运行并指出一个明显的图像问题。 回顾一下之前那个算法 我们今天要做一点预加载的处理。上周刚完成了游戏序章部分的所有剪辑内容。在运行这一部分时,如果观察得足够仔细,就会注意到一个问题。虽然因为视频流压缩质量较低,很难清楚…...
SAP-ABAP:SAP HANA高可用与灾备——存储镜像与系统复制的核心技术
SAP HANA作为企业关键业务的核心数据库,其高可用性(High Availability, HA)与灾备(Disaster Recovery, DR)能力直接影响业务连续性。HANA通过存储镜像、系统复制及集群集成三大核心技术,实现秒级故障切换与…...
工厂能耗系统智能化解决方案 —— 安科瑞企业能源管控平台
安科瑞顾强 政策背景与“双碳”战略驱动 2025年《政府工作报告》明确提出“单位国内生产总值能耗降低3%左右”的目标,要求通过产业结构升级(如高耗能行业技术革新或转型)、能源结构优化(提高非化石能源占比)及数字化…...
【pytorch图像视觉】lesson17深度视觉应用(上)构建自己的深度视觉项目
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、 数据1、认识经典数据1.1入门数据:MNIST、其他数字与字母识别(1)数据下载(2)查看数据的特征和标…...
java中的Future的设计模式 手写一个简易的Future
案例 例如:今天是小妹的生日,需要一个蛋糕有点仪式感,于是去蛋糕店预定,预定完之后,店老板说蛋糕做好了,到时电话通知你,不可能在这傻傻的等着吧,还有其他事情要做啊,于…...
USB(TYPE-C)转串口(TTL)模块设计讲解
目录 一 、引言 二、方案设计 三、USB TYPE-C介绍 1、TYPE-C接口定义 1、24P全引脚描述 2、Type C 接口 VBUS/GND 作用 3、Type C 接口 D/D- 作用 1、数据传输: 2、设备识别: 3、充电协议协商: 4、Type C 接口 CC1/CC2 作用 1、主从设备区…...
JavaScript | ajax实现原理
在早期,web应用,更多采用mvc框架,通过后端输出整个页面的内容,然后再用浏览器进行渲染,这样效率不高,对于事件绑定来说比较麻烦,于是提出了ajax,其最大的特点就是能实现局部更新。通…...
PyTorch张量操作指南:cat、stack、split与chunk的实战拆解
本文深入探讨PyTorch中用于调整张量结构的四个核心函数——torch.cat、torch.stack、torch.split和torch.chunk。通过实际应用场景分析和代码演示,帮助读者掌握它们的功能差异及适用条件,提升模型开发的灵活性与效率。 在深度学习实践中,张量…...
YOLO涨点技巧之分层扩展路径聚合网络 (HEPAN)
一、应用场景与问题背景 1.1 无人机图像检测挑战 https://ai-studio-static-online.cdn.bcebos.com/3d4f7e8c4d8d4d2d8a4c8e4b4e8c4d8d 场景特点:无人机航拍视角下的小目标检测(如行人、车辆、农作物病害等)核心难点: 目标尺寸小(<3232像素)复杂背景干扰(如城市…...
SQLite、MySQL、SQL Server、Oracle 和 PostgreSQL 五种数据库的区别
以下是 SQLite、MySQL、SQL Server、Oracle 和 PostgreSQL 五种主流关系型数据库管理系统(RDBMS)的区别,从多个维度进行对比: 1. 架构与部署 SQLite(Structured Query Language Lite): 嵌入式数据库,无服务器架构。数据库存储在一个单一的磁盘文件中。部署简单,适合轻量…...
git在分支上会退到某个指定的commit
1、在idea上先备份好分支(基于现有分支new branch) 2、在gitlab管理端删除现有分支 3、在idea中大卡terminal,执行 git log 查看commit log ,找到要会退到的commit唯一码,然后执行git reset 唯一码 4、查看本地代码状态 git st…...
玩机进阶教程----MTK芯片设备刷机导致的死砖修复实例解析 连电脑毫无反应 非硬件问题
在高通芯片机型中,我们可以通过短接主板测试点来激活高通芯片特有的9008底层端口来刷写救砖固件。但通常MTK芯片类的设备联机电脑即可触发深刷模式。但有些例外的情况会导致链接电脑毫无反应。遇到类似故障的友友可以参阅此博文尝试解决。 通过博文了解 1💝💝💝-----实…...
MIPI协议介绍
MIPI协议介绍 mipi 协议分为 CSI 和DSI,两者的区别在于 CSI用于接收sensor数据流 DSI用于连接显示屏 csi分类 csi 分为 csi2 和 csi3 csi2根据物理层分为 c-phy 和 d-phy, csi-3采用的是m-phy 一般采用csi2 c-phy 和 d-phy的区别 d-phy的时钟线和数据线是分开的,2根线一对…...
MySQL 中 `${}` 和 `#{}` 占位符详解及面试高频考点
文章目录 一、概述二、#{} 和 ${} 的核心区别1. 底层机制代码示例 2. 核心区别总结 三、为什么表名只能用 ${}?1. 预编译机制的限制2. 动态表名的实现 四、安全性注意事项1. ${} 的风险场景2. 安全实践 五、面试高频考点1. 基础原理类问题**问题 1**:**问…...
AI应用开发平台 和 通用自动化工作流工具 的详细对比,涵盖定义、核心功能、典型工具、适用场景及优缺点分析
以下是 AI应用开发平台 和 通用自动化工作流工具 的详细对比,涵盖定义、核心功能、典型工具、适用场景及优缺点分析: 1. AI应用开发平台 vs 通用自动化工作流工具 (1) 定义与目标 类型AI应用开发平台通用自动化工作流工具定义用于快速构建、训练、部署…...
GitHub 趋势日报 (2025年04月12日)
本日报由 TrendForge 系统生成 https://trendforge.devlive.org/ 📈 今日整体趋势 Top 10 排名项目名称项目描述今日获星总星数语言1yeongpin/cursor-free-vip[Support 0.48.x](Reset Cursor AI MachineID & Auto Sign Up / In & Bypass Higher…...
asm汇编源代码之-字库转换程序
将标准的16x16点阵汉字库(下载16x16汉字库)转换成适合VGA文本模式下显示的点阵汉字库 本程序需要调用file.asm中的子程序,所以连接时需要把file连接进来,如下 C:\> tlink chghzk file 调用参数描述如下 C:\> chghzk ; 无调用参数,转换标准库文件(SRC16.FNT)为适合VGA…...
VMware Ubuntu挂载Windows机器的共享文件
https://www.dong-blog.fun/post/2029 在VMware Ubuntu中访问Windows共享文件夹:完整指南 在使用VMware运行Ubuntu虚拟机时,访问Windows主机上的文件是常见需求。本文将详细介绍如何通过网络共享方式,让Ubuntu虚拟机直接访问Windows主机的文…...
智慧社区数据可视化中枢平台——Axure全场景交互式大屏解决方案
在数字化治理的时代浪潮中,社区管理正面临数据碎片化、响应滞后、决策盲区等核心挑战。如何将分散的安防、环境、能源、民生服务等数据整合为可操作的智慧洞察?如何让冰冷的数字转化为社区管理者手中的决策利器?Axure智慧社区可视化大屏原型模…...
Axure高保真AI算法训练平台
点击下载《Axure高保真AI算法训练平台(.rp) 》 原型效果:https://axhub.im/ax9/69fdf8f2b10b59c3/#g1 摘要 本文介绍了一款功能全面且高效的AI算法训练平台,旨在为数据科学家、研究人员和工程师提供从数据准备到模型部署的一站式解决方案。该平台由四大…...
C++ Json-Rpc框架-3项目实现(2)
一.消息分发Dispatcher实现 Dispatcher 就是“消息分发中枢”:根据消息类型 MType,把消息派发给对应的处理函数(Handler)执行。 初版: #pragma once #include "net.hpp" #include "message.hpp"n…...
youtube视频和telegram视频加载原理差异分析
1. 客户侧缓存与流式播放机制 流式视频应用(如 Netflix、YouTube)通过边下载边播放实现流畅体验,其核心依赖以下技术: 缓存预加载:客户端在后台持续下载视频片段(如 DASH/HLS 协议的…...
LLM小白自学笔记:1.两种指令微调
一、LoRA 简单来说,LoRA不直接调整个大模型的全部参数(那样太费资源),而是在模型的某些层(通常是注意力层)加个“旁路”——两个小的矩阵(低秩矩阵)。训练时只更新这俩小矩阵&#x…...
【NLP】 19. Tokenlisation 分词 BPE, WordPiece, Unigram/SentencePiece
1. 翻译系统性能评价方法 在机器翻译系统性能评估中,通常既有人工评价也有自动评价方法: 1.1 人工评价 人工评价主要关注以下几点: 流利度(Fluency): 判断翻译结果是否符合目标语言的语法和习惯。充分性…...
OpenAI发布GPT-4.1系列模型——开发者可免费使用
OpenAI刚刚推出GPT-4.1模型家族,包含GPT-4.1、GPT-4.1 Mini和GPT-4.1 Nano三款模型。重点是——现在全部免费开放! 虽然技术升级值得关注,但真正具有变革意义的是开发者能通过Cursor、Windsurf和GitHub Copilot等平台立即免费调用这些模型。…...
各地物价和生活成本 东欧篇
东欧地区的物价差异相对较大,一些国家的物价较高,而另一些国家则相对便宜。这些差异主要受当地经济发展水平、工资水平、旅游业发展以及国际关系等因素影响。以下是一些典型的东欧国家,按物价高低进行分类: 🌍 物价较高…...
Vue —— 实用的工具函数
目录 响应式数据管理1. toRef 和 torefs2. shallowRef 和 shallowReactive3. markRaw 依赖追踪与副作用1. computed2. watch 和 watchEffect 类型判断与优化1. unref2. isRef 、isReactive 和 isProxy 组件通信与生命周期1. provide 和 inject2. nextTick 高级工具1. useAttrs …...
flex布局(笔记)
弹性布局(Flex布局)是一种现代的CSS布局方式,通过使用display: flex属性来创建一个弹性容器,并在其中使用灵活的盒子模型来进行元素的排列和定位。 主轴与交叉轴:弹性容器具有主轴(main axis)和…...
