同步异步日志系统
- 一、日志器模块设计
- 1.1 同步日志器模块设计
- 1.1.1 局部日志器建造者模式设计
- 1.1.2 同步日志器基本功能测试
- 1.2 异步日志器模块设计
- 1.2.1 单缓冲区设计
- 1.2.2 异步工作线程的设计(双缓冲区思想)
- 1.2.3 异步日志器设计
- 1.2.4 异步日志器建造者模式设计
- 1.2.5 异步日志器基本功能测试
一、日志器模块设计
功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。
管理的成员:
1.格式化模块对象
2.落地模块对象
3.默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
4.互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
5.日志名称(日志器的唯一标识,方便查找)
提供的操作:
debug等级日志的输出操作(分别封装日志消息LogMsg——各个接口日志等级不同)
info等级日志的输出操作
warn等级日志的输出操作
error等级日志的输出操作
fatal等级日志的输出操作
实现:
1.实现Logger基类(派生出同步日志器和异步日志器)
2.因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
3.模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作
当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:
同步日志器:直接对日志消息进行输出
异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出
因此:日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。
1.1 同步日志器模块设计
- 同步日志器的设计和具体框架
/*日志器模块
1.先设计一个日志器的基类
2.根据基类派生出不同的日志器
*/
#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
namespace logslearn
{
// 设计日志器基类
class Logger
{
// 公有
public:
// 基类指针,用来控制继承子类的对象
using ptr = std::shared_ptr<Logger>;
// 操作方法
// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级
void debug(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作
void info(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作
void warn(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作
void error(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作
void fatal(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作
protected:
// 日志落地,抽象接口完成实际的落地输出——不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;
protected:
std::mutex _mutex; // 互斥锁
std::string _logger_name; // 日志器的名字
std::atomic<loglevel::value> _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks; // 这是一个数组,数组里存放日志落地方式的对象
};
// 派生出同步日志器
class SyncLogger : public Logger
{
protected:
// 重写虚函数
void log(const char *data, size_t len)
{
}
};
}
- 同步日志器的具体实现
#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
/*日志器模块
1.先设计一个日志器的基类
2.根据基类派生出不同的日志器
*/
#define _GNU_SOURCE
#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
#include <cstdarg>
namespace logslearn
{
// 设计日志器基类
class Logger
{
// 公有
public:
// 基类指针,用来控制继承子类的对象
using ptr = std::shared_ptr<Logger>;
// 构造函数
Logger(const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {}
// 操作方法
//获取日志器名称
const std::string& name(){
return _logger_name;
}
// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (loglevel::value::DEBUG < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(loglevel::value::DEBUG, file, line, res);
free(res); // 将指针释放掉
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (loglevel::value::INFO < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(loglevel::value::INFO, file, line, res);
free(res); // 将指针释放掉
}
void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (loglevel::value::WARN < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(loglevel::value::WARN, file, line, res);
free(res); // 将指针释放掉
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (loglevel::value::ERROR < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(loglevel::value::ERROR, file, line, res);
free(res); // 将指针释放掉
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (loglevel::value::FATAL < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(loglevel::value::FATAL, file, line, res);
free(res); // 将指针释放掉
}
void serialize(loglevel::value level, const std::string &file, size_t line, char *str)
{
// 3.构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, str); // 传入等级、行号、文件、日志器、有效信息
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5.进行日志落地
log(ss.str().c_str(), ss.str().size()); // 日志字符串和长度
}
// 日志落地,抽象接口完成实际的落地输出——不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;
protected:
std::mutex _mutex; // 互斥锁
std::string _logger_name; // 日志器的名字
std::atomic<loglevel::value> _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks; // 这是一个数组,数组里存放日志落地方式的对象
};
// 派生出同步日志器
class SyncLogger : public Logger
{
public:
// 构造函数
SyncLogger(const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : Logger(logger_name, level, formatter, sinks) {}
protected:
// 重写虚函数,同步日志器是将日志通过落地模块句柄进行日志落地
void log(const char *data, size_t len)
{
std::unique_lock<std::mutex> lock(_mutex);
// 是空
if (_sliks.empty())
{
return;
}
// 不是空
for (auto &sink : _sliks)
{
sink->log(data, len);
}
}
};
}
#endif
完成了日志器模块抽象基类的接口实现以及日志器的实际落地功能
3. 对同步日志器进行测试
测试各个接口是否可以编译成功
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
// 日志器模块:同步日志器
std::string logger_name = "sync_logger";
logslearn::loglevel::value limit = logslearn::loglevel::value::WARN;
logslearn::Formatter::ptr fmt(new logslearn::Formatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"));
logslearn::LogSink::ptr stdout_lsp = logslearn::SinkFactory::create<logslearn::StdoutSink>(); // 标准输出落地
logslearn::LogSink::ptr file_lsp = logslearn::SinkFactory::create<logslearn::FileSink>("./logfile/test.log"); // 文件落地方式
logslearn::LogSink::ptr roll_lsp = logslearn::SinkFactory::create<logslearn::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
std::vector<logslearn::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
logslearn::Logger::ptr logger(new logslearn::SyncLogger(logger_name, limit, fmt, sinks));
logger->debug(__FILE__, __LINE__, "%s", "测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warn(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t cursize = 0;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
测试结果符合要求(1024102410/20)
1.1.1 局部日志器建造者模式设计
- 局部日志器建造者模式的设计和框架分析。(同步日志器模块)
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
// 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度
// 1.抽象一个日志器建造者类
// 1.设置日志器类型
// 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。
class LoggerBuilder
{
public:
void buildLoggerType(LoggerType type);
void buildLoggerName(std::string &name);
void buildLoggerLevel(loglevel::value level);
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern);
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg);
// 完成我们的日志器构建
virtual void build() = 0;
private:
// 需要管理的数据,也就是建造的零部件
LoggerType logger_type; // 日志器类型
std::string _logger_name; // 日志器的名字
loglevel::value _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks;
};
// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)
// 局部日志器的建造者
class LocalLoggerBuilder : public LoggerBuilder
{
public:
void build() override;
};
- 实现具体操作的各个接口。
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
// 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度
// 1.抽象一个日志器建造者类
// 1.设置日志器类型
// 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。
class LoggerBuilder
{
public:
// 构造函数
LoggerBuilder() : logger_type(LoggerType::LOGGER_SYNC),
_limit_level(logslearn::loglevel::value::DEBUG) {}
void buildLoggerType(LoggerType type)
{
logger_type = type;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(loglevel::value level)
{
_limit_level = level;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(arg)...); // 完美转发
_sliks.push_back(psink);
}
// 完成我们的日志器构建
virtual Logger::ptr build() = 0;
protected:
// 需要管理的数据,也就是建造的零部件
LoggerType logger_type; // 日志器类型
std::string _logger_name; // 日志器的名字
loglevel::value _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks;
};
// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)
// 局部日志器的建造者
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty()==false);
// 必须要有formatter//必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式就给它添加一个标准输出的默认落地方式
if (_sliks.empty())
{
buildSink<StdoutSink>();
}
// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器
if (logger_type == LoggerType::LOGGER_ASYNC)
{
// 目前不做处理,后面在写
}
// 返回同步日志器的对象
return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sliks); // r日志器名字,等级,格式化,落地方式
}
};
1.1.2 同步日志器基本功能测试
我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只要构造就可以了,所有只有建造者。
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
//同步日志器建造者模式的测试
//先要构造一个建造者出来
std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(logslearn::LoggerType::LOGGER_SYNC);
builder->buildLoggerName("sync_logger");
builder->buildLoggerLevel(logslearn::loglevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<logslearn::StdoutSink>(); // 标准输出落地
builder->buildSink<logslearn::FileSink>("./logfile/test.log"); // 文件落地方式
builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
logslearn::Logger::ptr logger=builder->build();
//测试日志打印
logger->debug(__FILE__, __LINE__, "%s", "测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warn(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t cursize = 0;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
总结:同步日志器直接将日志消息进行格式化写入文件。
1.2 异步日志器模块设计
思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)
实现:
1.实现一个线程安全的缓冲区
2.创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。
缓冲区详情设计:
1.使用队列缓存日志消息,逐条处理
要求:不能涉及空间的频繁申请与释放,否则会降低效率。
结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
线程安全实现:对于缓冲区的读写加锁
因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
问题:锁冲突较为严重,所有线程之间都存在互斥关系
解决方案:双缓冲区
避免了空间频繁的申请与释放,释放与申请的频率,减少了生产者和消费者之间的锁冲突,提高了效率。
1.2.1 单缓冲区设计
设计一个缓冲区:直接存放格式化后的日志消息字符串
好处:
1.减少了LogMsg对象频繁的构造的消耗
2.可以针对缓冲区中的日志消息,一次性进行I0操作,减少I0次数,提高效率
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供的操作:
1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化——将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)
- 第一步,先设计类,把类的框架搭建出来
/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
// 异步缓冲区
class Buffer
{
public:
// 构造函数
Buffer() {}
// 1.向缓冲区写入数据
void push(const char *data, size_t len);
// 2.返回可读数据起始地址的接口
const char *begin();
// 3.返回可读数据的长度的接口;返回可写数据的长度的接口
size_t readAbleSize();
size_t writeAbleSize();
// 4.移动读写指针进行向后偏移的接口
void moveWriter(size_t len);
void moveReader(size_t len);
// 5.重置读写位置,初始化缓冲区的操作
void reset();
// 6.交换缓冲区的接口
void swap( Buffer &buffer);
// 判断缓冲区是否为空
bool empty();
private:
// 1.存放字符串数据的缓冲区
std::vector<char> _buffer;
// 2.当前可写数据的指针--本质是下标
size_t _reader_idx;
// 3.当前可读数据的指针
size_t _writer_idx;
};
}
- 实现类的各个功能
#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
#include <cassert>
#include <algorithm>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
// 异步缓冲区
class Buffer
{
public:
// 构造函数
Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
// 1.向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞)
void push(const char *data, size_t len)
{
// 缓冲区剩余空间不够的情况下:扩容。
// // 1.固定大小,直接返回
// if (len > writeAbleSize())
// return;
// 2.动态空间,用于极限测试--扩容
ensureEnoughSize(len);
// 2.将数据拷贝到缓冲区
std::copy(data, data + len, &_buffer[_writer_idx]);
// 3.将当前写入位置向后偏移
moveWriter(len);
}
// 2.返回可读数据起始地址的接口
const char *begin()
{
return &_buffer[_reader_idx];
}
// 3.返回可读数据的长度的接口;返回可写数据的长度的接口
size_t readAbleSize()
{
// 返回可读数据的长度
// 因为当前实现的缓冲区设计思想是双缓冲区,处理完就交换,所以不存在空间循环使用
return (_writer_idx - _reader_idx);
}
size_t writeAbleSize()
{
// 对于扩容的思路来说,不存在可写空间大小,因为总是可写的。
// 因此这个接口只提供给固定大小缓冲区。
return (_buffer.size() - _writer_idx);
}
// 4.移动读写指针进行向后偏移的接口
void moveWriter(size_t len)
{
// 移动可写指针
assert((_writer_idx + len) <= _buffer.size());
_writer_idx += len;
}
void moveReader(size_t len)
{
// 移动可读指针
assert(len <= readAbleSize()); // len大就报错
_reader_idx += len;
}
// 5.重置读写位置,初始化缓冲区的操作
void reset()
{
// 读写位置都为零
_writer_idx = 0; // 缓冲区所有空间都是空闲得
_reader_idx = 0; //_reader_idx与_writer_idx相等就表示没有数据可以读
}
// 6.交换缓冲区的接口
void swap(Buffer &buffer)
{
// 交换缓冲区地址
_buffer.swap(buffer._buffer);
// 读写位置的地址也要交换
std::swap(_reader_idx, buffer._reader_idx);
std::swap(_writer_idx, buffer._writer_idx);
}
// 判断缓冲区是否为空
bool empty()
{
return (_reader_idx == _writer_idx); // 位置一样就是空
}
private:
// 对空间进行扩容
void ensureEnoughSize(size_t len)
{
// 不需要扩容
if (len <= writeAbleSize())
return;
size_t new_size = 0;
if (_buffer.size() < THRESHOLD_BUFFER_SIZE)
{
// 小于阈值则翻倍增长
new_size = _buffer.size() * 2;
}
else
{
new_size = _buffer.size() + INCREMENT_BUFFER_SIZE+len; // 否则线性增长
}
// 重新调整空间大小
_buffer.resize(new_size);
}
private:
// 1.存放字符串数据的缓冲区
std::vector<char> _buffer;
// 2.当前可写数据的指针--本质是下标
size_t _reader_idx;
// 3.当前可读数据的指针
size_t _writer_idx;
};
}
#endif
- 对异步缓冲区进行代码完善和功能测试
测试问题:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include <fstream>
int main()
{
// 异步日志器缓冲区测试
// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件
if (ifs.is_open() == false)
{
return -1;
} // 文件打开失败返回-1
// 让读写位置跳转到末尾
ifs.seekg(0, std::ios::end);
// 获取当前读写位置相对于起始位置的偏移量
size_t fsize = ifs.tellg();
// 重新让指针跳转到起始位置
ifs.seekg(0, std::ios::beg);
std::string body;
body.resize(fsize);
ifs.read(&body[0], fsize);
if (ifs.good() == false)
{
std::cout << "read error!\n";
return -1;
}
// 打开文件,就要关闭文件
ifs.close();
std::cout << fsize << std::endl; // 读取文件的数据大小
logslearn::Buffer buffer; // 定义一个缓冲区
for (int i = 0; i < body.size(); i++)
{
buffer.push(&body[i], 1);
}
std::cout << buffer.readAbleSize() << std::endl; // buffer里面可读的数据大小
std::ofstream ofs("./logfile/tem.log", std::ios::binary);
// //一次性写入
// ofs.write(buffer.begin(),buffer.readAbleSize());
//错误
// for (int i = 0; i < buffer.readAbleSize(); i++)
// { // readAbleSize()可读数据的长度,可读数据在减少,i在++,双休套进的过程
// ofs.write(buffer.begin(), 1);
// if (ofs.good() == false)
// {
// std::cout << "writer error!\n";
// return -1;
// }
// buffer.moveReader(1);
// }
//正确
size_t rsize=buffer.readAbleSize();
for (int i = 0; i < rsize; i++)
{ // readAbleSize()可读数据的长度,可读数据在减少,i在++,双休套进的过程
ofs.write(buffer.begin(), 1);
if (ofs.good() == false)
{
std::cout << "writer error!\n";
return -1;
}
buffer.moveReader(1);
}
ofs.close(); // 关闭文件
return 0;
}
md5sum 可以验证文件的完整性,tem.log和test.log文件的md5值一样,说明文件内容一模一样
1.2.2 异步工作线程的设计(双缓冲区思想)
异步工作器:
1.异步工作使用双缓冲区的思想
外界将任务数据,添加到输入缓冲区中
异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区
实现:
管理的成员
1.双缓冲区(生产,消费)
2.互斥锁 // 保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
1.停止异步工作器
2.添加数据到缓冲区
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
- 设计异步工作器的框架,已经具体成员
/*
实现异步工作器
*/
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace logslearn
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper();
void stop();
void push(const char *data, size_t len);
private:
void threadEntry(); // 线程入口函数
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
// bool _stop;//工作器停止标准,如果工作器停止了,会存在线程安全问题
std::atomic<bool> _stop; // 让工作器停止标准变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量
std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
#endif
- 异步工作器的具体实现
/*
实现异步工作器
*/
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace logslearn
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险
ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASUNC_UNSAFE) : _looper_type(looper_type), _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)),
_callBack(cb) {}
~AsyncLooper() { stop(); }
void stop()
{
_stop = true; // 将退出标志设置为true
_cond_con.notify_all(); // 唤醒所有的工作线程,工作线程只有一个
_thread.join();//等待工作线程退出
}
void push(const char *data, size_t len)
{
// 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小
// 加个互斥锁
std::unique_lock<std::mutex> lock(_mutex);
// 条件变量空值,若缓冲区剩余空间大小大于数据长度,就可以添加数据
// 如果是安全状态就把这个代码加上,非安全状态就把这个代码屏蔽
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.wait(lock, [&]()
{ return _pro_buf.writeAbleSize() >= len; });
// 能够走下来代表满足了条件,可以向缓冲区添加数据
_pro_buf.push(data, len);
// 唤醒一个消费者对缓冲区中的数据进行处理
_cond_con.notify_one();
}
private:
// 线程入口函数--对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
void threadEntry()
{
while (1)
{
// 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(并不对数据的处理过程加锁保护)
{
// 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
//退出标志被设置,且生产缓冲区无数据,这时候在退出,否则有可能会造成生产缓冲区有数据,但是没有被完全处理
if (_stop&&_pro_buf.empty())
break;
// 若退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠
_cond_con.wait(lock, [&]()
{ return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换
// 等待完毕,消费者与生产者进行地址交换
_con_buf.swap(_pro_buf);
// 2.唤醒生产者
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
// 3.被唤醒后,对消费缓冲区进行数据处理
_callBack(_con_buf);
// 4.初始化消费缓冲区
_con_buf.reset();
}
}
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
// bool _stop;//工作器停止标准,如果工作器停止了,会存在线程安全问题
AsyncType _looper_type; // 默认是安全模式
std::atomic<bool> _stop; // 让工作器停止标准变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量
std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
#endif
测试的话,需要异步日志器设计好在测试
1.2.3 异步日志器设计
1.继承于Logger日志器类 对于写日志操作进行函数重写(不再将数据写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地
管理的成员:
1.异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建
- 异步日志器的框架设计
//派生出异步日志器
class AsyncLogger:public Logger{
public:
AsyncLogger( const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :
Logger(logger_name, level, formatter, sinks) {}
//将数据写入缓冲区
void log(const char*data,size_t len);
//设计一个实际落地函数(将缓冲区里的数据进行落地)
void realLog(Buffer &buf);
private:
AsyncLooper::ptr _looper;
};
- 完成异步日志器类的功能
//派生出异步日志器
class AsyncLogger:public Logger{
public:
AsyncLogger( const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :
Logger(logger_name, level, formatter, sinks),_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog,this,std::placeholders::_1),looper_type)) {}
//将数据写入缓冲区
void log(const char*data,size_t len){
//消息入队操作,不需要加锁,已经是线程安全得了
_looper->push(data,len);
}
//设计一个实际落地函数(将缓冲区里的数据进行落地)
void realLog(Buffer &buf){
if(_sliks.empty())return ;
for(auto &sink:_sliks){
sink->log(buf.begin(),buf.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
1.2.4 异步日志器建造者模式设计
1.本地局部的日志器建造者——异步日志器模块
- 局部日志器建造者功能的完善
这个类的完善,需要在异步日志器完成之后,才能完善
完善部分:
1.首先要异步日志器完成
2.建造者需要再加一个默认模式的零件,以及零部件的建造
3.完善构造函数
4.异步日志器对象的实例化
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
// 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度
// 1.抽象一个日志器建造者类
// 1.设置日志器类型
// 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。
class LoggerBuilder
{
public:
// 构造函数
LoggerBuilder() : logger_type(LoggerType::LOGGER_SYNC),
_limit_level(logslearn::loglevel::value::DEBUG),
_looper_type(AsyncType::ASYNC_SAFE) {}
void buildEnabeUnSafeAsync()
{
_looper_type = AsyncType::ASUNC_UNSAFE; // 非安全模式
}
void buildLoggerType(LoggerType type)
{
logger_type = type;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(loglevel::value level)
{
_limit_level = level;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(arg)...); // 完美转发
_sliks.push_back(psink);
}
// 完成我们的日志器构建
virtual Logger::ptr build() = 0;
protected:
// 需要管理的数据,也就是建造的零部件
AsyncType _looper_type; // 异步线程的安全模式
LoggerType logger_type; // 日志器类型
std::string _logger_name; // 日志器的名字
loglevel::value _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks;
};
// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)
// 局部日志器的建造者
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty() == false);
// 必须要有formatter//必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式就给它添加一个标准输出的默认落地方式
if (_sliks.empty())
{
buildSink<StdoutSink>();
}
// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器
if (logger_type == LoggerType::LOGGER_ASYNC)
{
// 返回异步日志器对象
return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sliks,_looper_type);
}
// 返回同步日志器的对象
return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sliks); // r日志器名字,等级,格式化,落地方式
}
};
}
1.2.5 异步日志器基本功能测试
异步工作器与异步日志器一起联调。
测试:用建造者模式,构建对象,选择异步日志器,并且把10万条日志打印在文件和屏幕上
set nu//看文件里数据的行号
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include "looper.hpp"
#include <fstream>
int main()
{
//异步日志器的测试
//异步日志器和异步工作器进行联调
// //先要构造一个建造者出来
std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(logslearn::LoggerType::LOGGER_ASYNC);
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(logslearn::loglevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<logslearn::StdoutSink>(); // 标准输出落地
builder->buildSink<logslearn::FileSink>("./logfile/async.log"); // 文件落地方式
// builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
logslearn::Logger::ptr logger=builder->build();
//测试日志打印
logger->debug(__FILE__, __LINE__, "%s", "测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warn(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t count = 0;
while (count < 100000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
return 0;
}