双缓冲区异步任务处理器(AsyncLooper)设计
设计思想:异步处理线程+数据池
使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。
在任务池的设计中,有很多备选方案,比如循环队列等等,但是不管是哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每一次的任务添加与取出都有可能涉及锁的冲突,而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程写入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
/*
前面完成的是同步日志器:直接将日志消息进行格式化写入文件
接下来完成的是异步日志器:
思想为了避免写日志的过程阻塞导致业务线程在写日志的时候影响效率,因此异步的思想就是不让业务线程进行日志的实际落地操作,
而是将日志消息放到缓冲区中(一块指定的内存中),接下来有一个专门异步线程,去针对缓冲区中的数据进行处理。
实现:
1. 现一个线程安全的缓冲区
2. 创建一个异步工作的线程,专门负责缓冲区日志消息的落地操作
缓冲区的详细设计:
1. 使用队列,缓存日志消息逐条处理
要求:不能涉及到空间的频繁申请与释放,否则会降低效率
结果:设计一个环形的队列(提前向空间申请好,然后对空间循环利用)
问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全 -- 对于缓冲区的读写加锁
因为写日志操作,在实际开发中,并不会分配太多的资源,所以工作线程只需要有一个日志器就行
涉及到的锁冲突:生产者与生产者 & 消费者与消费者
问题:锁冲突较为严重,因为所有线程之间都存在互斥关系
解决方案:双缓冲区阻塞数据池 优势--避免了空间的频繁申请与释放,且尽可能的减少了生产者与消费者之间的锁冲突的概率,提高了任务处理效率
单个缓冲区的进一步设计:
设计一个缓冲区:直接存放格式化后的日志消息字符串
好处:
1. 减少了LogMsg对象频繁的构造消耗
2. 可以减少缓冲区中的日志消息,一次性的进行IO操作,减少IO次数,提高效率
缓冲区类的设计:
1. 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)
2. 当前的写入数据位置的指针(指向可写区域的起始位置,避免过数据的写入覆盖)
3. 当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供的操作:
1. 向缓冲区写入数据
2. 从缓冲区读取数据(读取指定长度数据,或者读取所有数据) -- 获取可读数据起始地址的接口
3. 获取可读数据长度的接口
4. 移动读写位置的接口
5. 初始化缓冲区的操作(将读写位置初始化 -- 将缓冲区所有数据处理完毕之后)
6. 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)
*/
/*实现异步日志缓冲区*/
namespace zyqlog
{
#define DEFAULT_BUFFER_SIZE (10 * 1024 * 1024) // 缓冲区的默认大小 10M
#define THRESHOLD_BUFFER_SIZE (80 * 1024 *1024) // 扩容阈值大小,小于阈值翻倍增长
#define INCREMENT_BUFFER_SIZE (10 * 1024 * 1024) // 线性增长大小
class Buffer
{
public:
Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _writer_idx(0), _reader_idx(0) {}
void push(const char *data, size_t len) // 想缓冲区中写入数据
{
// 缓冲区剩余空间不够:1. 扩容 2. 阻塞/返回false
// 1. 固定大小 直接返回
// if (len > writeAbleSize()) return ;
// 2. 动态空间用于极限性能测试 -- 扩容
ensureEnoughSize(len);
// 1. 将数据拷贝进入缓冲区
std::copy(data, data + len, &_buffer[_writer_idx]);
// 2. 将当前写入位置向后偏移
moveWriter(len);
}
const char* begin() // 返回可读数据的起始地址
{
return &_buffer[_reader_idx];
}
size_t writeAbleSize() // 返回可写数据的长度
{
// 对于扩容思路来说不存在可写空间大小,因为总是可写
// 因此这个接口仅仅针对固定大小缓冲区提供操作
return (_buffer.size() - _writer_idx);
}
size_t readAbleSize() // 返回可读数据的长度
{
// 因为当前实现的缓冲区设计思想是双缓冲区,处理完就交换不存在空间循环使用的情况
return (_writer_idx-_reader_idx);
}
void moveReader(size_t len) // 读写指针进行向后偏移操作
{
assert(len <= readAbleSize());
_reader_idx += len;
}
void reset() // 重置读写位置,初始化缓冲区
{
_reader_idx = 0; // 缓冲区所有空间都是空闲的
_writer_idx = 0; // 与_writer_idx相等表示没有数据可读
}
void swap(Buffer &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 + len; // 小于阈值翻倍增长
}
else
{
new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len; // 否则线性增长
}
_buffer.resize(new_size);
}
void moveWriter(size_t len) // 读写指针进行向后偏移操作
{
assert((len + _writer_idx) <= _buffer.size());
_writer_idx += len;
}
private:
std::vector<char> _buffer;
size_t _reader_idx; // 当前可读数据的指针--本质是下标
size_t _writer_idx; // 当前可写数据的指针
};
}
// 双缓冲区异步任务处理器 test
int main()
{
std::ifstream ifs("./logfile/test.log", std::ios::binary);
if (ifs.is_open() == false)
{
std::cout << "open failed!" << std::endl;
return -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!" << std::endl;
return -1;
}
ifs.close();
std::cout << fsize << std::endl;
zyqlog::Buffer buffer;
for(int i = 0; i < body.size(); i++)
{
buffer.push(&body[i], 1);
}
std::ofstream ofs("./logfile/tmp.log", std::ios::binary);
// ofs.write(buffer.begin(), buffer.readAbleSize()); // 一次性将所有的数据读取
// 逐字节进行读取
for(int i = 0; i < buffer.readAbleSize();) // 因为readAbleSize()在逐渐的变小,i++同时又在放大,双向变化会有问题
{
ofs.write(buffer.begin(), 1);
buffer.moveReader(1);
}
ofs.close();
}
/*
异步工作器的设计:
1. 异步工作使用双缓冲区思想
外界将任务数据,添加到输入缓冲区中,异步线程对处理缓冲区中的数据进行处理,若缓冲区中没有数据了则交换缓冲区
实现:
管理的成员:
1. 双缓冲区(生产消费)
2. 互斥锁 保证线程安全
3. 条件变量-生产和消费 (生产线程没有数据,处理完消费缓冲区数据后就休眠)
4. 回调函数(针对缓冲区中的数据的处理接口-外界传入一个函数,告诉异步工作器如何处理)
提供的操作:
1. 停止异步工作器
2. 添加数据到缓冲区
私有操作:
创还能线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再交换
*/
/*实现异步工作器*/
namespace zyqlog
{
using Functor = std::function<void (Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态表示缓冲区满了则阻塞,避免资源耗尽的风险
ASYNC_UNSAFE // 不考虑资源耗尽的问题,无线扩容,常用于测试
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType loop_type = AsyncType::ASYNC_SAFE)
: _stop(false)
, _looper_type(loop_type)
, _thread(std::thread(&AsyncLooper::threadEntry, this))
, _callBack(cb)
{}
~AsyncLooper()
{
stop();
_thread.join(); // 等待工作线程的退出
}
void stop()
{
_stop = true; // 将退出标志设置为true;
_cond_con.notify_all(); // 唤醒所有的工作线程
}
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(true)
{
// 为互斥锁设置一个生命周期,当缓冲区交换完毕之后就解锁(并不对数据处理过程加锁保护)
{
// 1. 判断生产缓冲区中有没有数据,有着交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
if (_stop == true && _pro_buf.empty()) break; // 退出标志被设置且生产缓冲区内无数据,再退出,否则有可能会造成生产缓冲区中有数据,但是没有被完全处理
_cond_con.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
_con_buf.swap(_pro_buf);
// 2. 唤醒生产者
if (_looper_type == AsyncType::ASYNC_SAFE) // 安全状态才需要唤醒
_cond_pro.notify_all();
}
// 3. 被唤醒后,对消费缓冲区进行处理
_callBack(_con_buf);
// 4. 初始化消费缓冲区
_con_buf.reset();
}
}
Functor _callBack; // 对缓冲区数据进行处理的回调函数,由异步工作器使用者传入
private:
AsyncType _looper_type;
std::atomic<bool> _stop; // 工作器停止标志
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex;
std::condition_variable _cond_pro; // 条件变量
std::condition_variable _cond_con; // 条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
异步日志器(AsyncLogger)设计
异步日志器类继承自日志器类,并在同步日志器类上拓展了异步消息处理器。当我们需要异步输出日志的时候,需要创建异步日志器和消息处理器,调用异步日志器的log、error、info、fatal等函数输出不同级别日志。
- log函数为重写Logger类的函数,主要实现将日志数据加入异步队列缓冲区中
- realLog函数主要由异步线程进行调用(是为异步消息处理器设置的回调函数),完成日志的实际落地工作。
/*
异步日志器设计
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)
, _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type))
{}
// 将数据写入缓冲区
void log(const char *data, size_t len) override
{
_looper->push(data, len);
}
// 设计一个实际落地函数(将缓冲区中的数据落地)
void realLog(Buffer &buf)
{
if (_sinks.empty()) return ;
for (auto &sink : _sinks)
{
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
单例日志器管理类设计(单例模式)
日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。
因此,为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。
基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。
/*
日志器管理器
作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例
作用2:在程序的任意位置获取相同的单例对象,获取其中的日志器进行日志输出
拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)
目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用
设计:
管理的成员:
1. 默认日志器
2. 所管理的日志器数组
3. 互斥锁
提供的接口:
1. 添加日志器管理
2. 判断是否管理了指定名称的日志器
3. 获取指定名称的日志器
4. 获取默认日志器
*/
class LoggerManager
{
public:
static LoggerManager& getInstance()
{
// 在C++11之后,针对静态局部变量,编译器在编译的层面实现了线程安全
// 当静态局部变量没有构造完成之前,其他线程就会阻塞
static LoggerManager eton;
return eton;
}
void addLogger(Logger::ptr &logger)
{
if (hasLogger(logger->name())) return ;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(logger->name(), logger));
}
bool hasLogger(const std::string &name)
{
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return false;
}
return true;
}
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()
{
return _root_logger;
}
private:
LoggerManager()
{
std::unique_ptr<zyqlog::LoggerBuilder> builder(new zyqlog::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger = builder->build();
_loggers.insert(std::make_pair("root", _root_logger));
}
private:
std::mutex _mutex;
Logger::ptr _root_logger; // 默认日志器
std::unordered_map<std::string, Logger::ptr> _loggers;
};
// 设计一个全局日志器的建造者--在局部的基础上增加了一个功能:将日志器添加到单例对象中
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
assert(!_logger_name.empty()); // 必须有日志器名称
if (_formatter.get() == nullptr) // 用户没有传入日志器的输出格式,需要给以一个默认格式
{
_formatter = std::make_shared<ForMatter>();
}
if (_sinks.empty()) // 用户没有指定落地方式,这里有我们自己进行指定
{
buildSink<StdoutSink>();
}
Logger::ptr logger;
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
logger = std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
else
{
logger = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
LoggerManager::getInstance().addLogger(logger);
return logger;
}
};
}
日志宏&全局接口设计(代理模式)
提供全局的日志器获取接口。
使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
当仅需标准输出日志的时候可以通过主日志器来打印日志。且操作时只需要通过宏函数直接进行输出即可。
/*
提供全局接口&宏函数,对日志系统接口进行使用便捷性优化
思想:
1. 提供获取制定日志器的全局接口(避免用户自己操作单例对象)
2. 使用宏函数对日志器的接口进行代理(代理模式)
3. 提供宏函数直接进行日志的标准输出的打印(不同获取日志器)
*/
namespace zyqlog
{
// 1. 提供获取制定日志器的全局接口(避免用户自己操作单例对象)
Logger::ptr getLogger(const std::string &name)
{
return zyqlog::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger()
{
return zyqlog::LoggerManager::getInstance().rootLogger();
}
// 2. 使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warning(fmt, ...) warning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 3. 提供宏函数直接进行日志的标准输出的打印(不同获取日志器)
// #define DEBUG(logger, fmt, ...) logger->debug(fmt, ##__VA_ARGS__)
// #define DLOG(fmt, ...) DEBUG(rootLogger(), fmt, ##__VA_ARGS__) -> _root_logger->debug(fmt, ##__VA_ARGS__)
#define DEBUG(fmt, ...) zyqlog::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) zyqlog::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARNING(fmt, ...) zyqlog::rootLogger()->warning(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) zyqlog::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) zyqlog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
#endif
性能测试
下面对日志系统做一个性能测试,测试一下平均每秒能打印多少条日志消息到文件。主要的测试方法是:每秒能打印日志数=打印日志条数/总的打印日志消耗时间
主要测试要素:同步/异步&单线程/多线程
- 100w+条指定长度的日志输出所耗时间·每秒可以输出多少条日志
- 每秒可以输出多少MB日志
/*
测试三要素:
1. 测试环境
2. 测试方法
3. 测试结果 -- 同步下的单线程与多线程;异步下的单线程与多线程
测试工具的编写:
1. 可以控制写日志线程数量(控制写日志线程的数量)
2. 可以控制写日志的总数量
3. 分别对同步日志器 & 异步日志器进行各自的性能测试
需要测试单写日志线程的性能
需要测试多写日志线程的性能
实现:
封装一个接口,传入日志器的名称,线程数量,日志数量,单条日志大小,
在接口内创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,
在输出完毕后计时结束,所耗时间 = 结束时间 - 起始时间
每秒输出量 = 总日志数量 / 总时间
每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时
注意:异步日志这里,我们启动非安全模式,存内存写入(不去考虑实际的落地时间)
*/
#include "../logs/zyqlog.h"
#include <vector>
#include <thread>
#include <chrono>
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
// 1. 获取日志器
zyqlog::Logger::ptr logger = zyqlog::getLogger(logger_name);
if (logger.get() == nullptr)
{
return ;
}
std::cout << "测试日志:" << msg_count << " 条,总大小:" << (msg_count * msg_len) / 1024 << "KB\n";
// 2. 组织指定长度的日志消息
std::string msg(msg_len - 1, 'A'); // 少一个字节为了后面给末尾添加换行
// 3. 创建指定数量的线程
std::vector<std::thread> threads;
std::vector<double> cost_arry(thr_count);
size_t msg_per_thr = msg_count / thr_count; // 总日志数量 / 线程数量就是每隔线程要输出的日志数量
for (int i = 0; i < thr_count; ++i)
{
threads.emplace_back([&, i](){
// 4. 线程函数内部开始计时
auto start = std::chrono::high_resolution_clock::now();
// 5. 开始循环写日志
for (int j = 0; j < msg_per_thr; ++j)
{
logger->fatal("%s", msg.c_str());
}
// 6. 线程函数内部结束计时
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost = end - start;
cost_arry[i] = cost.count();
std::cout << "\t线程" << i << ": " << "\t输出日志数量:" << msg_per_thr << "\t耗时:" << cost.count() << "s" << std::endl;
});
}
for (int i = 0; i < thr_count; ++i)
{
threads[i].join();
}
// 7. 计算总耗时:在多线程中,每个线程都会耗费时间,但是线程是并发处理的,因此耗时最高的那个就是总时间
double max_cost = cost_arry[0];
for (int i = 0; i < thr_count; ++i)
{
max_cost = max_cost < cost_arry[i] ? cost_arry[i] : max_cost;
}
size_t msg_per_sec = msg_count / max_cost;
size_t size_per_sec = msg_count * msg_len / (max_cost * 1024);
// 8. 进行输出打印
std::cout << "\t总耗时:" << max_cost << "s\n";
std::cout << "\t每秒输出日志数量:" << msg_per_sec << "条\n";
std::cout << "\t每秒输出日志大小:" << size_per_sec << "KB\n";
}
// 多线程比单线程慢,单线程不涉及锁冲突,多线程会涉及到锁冲突,但是并没有提高多少效率,磁盘存在上限。
void sync_bench()
{
std::unique_ptr<zyqlog::LoggerBuilder> builder(new zyqlog::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildLoggerType(zyqlog::LoggerType::LOGGER_SYNC);
builder->buildFormatter("%m%n");
builder->buildSink<zyqlog::FileSink>("./logfile/sync.log");
builder->build();
bench("sync_logger", 3, 2000000, 100);
}
// 往内存中放,不考虑磁盘的性能,更多的考虑cpu内存的性能
void async_bench()
{
std::unique_ptr<zyqlog::LoggerBuilder> builder(new zyqlog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerType(zyqlog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();
builder->buildFormatter("%m%n");
builder->buildSink<zyqlog::FileSink>("./logfile/async.log");
builder->build();
bench("async_logger", 3, 2000000, 100);
}
int main()
{
sync_bench();
async_bench();
}
整个项目的gitee地址:https://gitee.com/zhengyiq/logger