【项目】多设计模式下的同步&&异步日志系统(二)

继上文对日志系统的介绍,并且实现了日志等级、日志消息的定义、以及格式化消息等。由这三个模块就能完整的输出一条消息。但是考虑到日志消息不可能全部都通过显示器展示。本项目针对落地方式,也进行多种编写,以供使用。

消息落地类(简单工厂模式)

消息落地的方式

  • 标准输出
  • 文件
  • 按照大小滚动文件
  • 按照时间滚动文件

考虑到如果将消息都放到文件中,查找起来不方便。如果文件过大,就要删除文件,之前的日志消息就会丢失。

针对落地到文件上的不足,博主设计滚动文件。一但到达某个要求之后,就打开一个新文件,将数据往新文件中写。

消息落地设计思路

落地类支持扩展,目前支持落地到标准输出、文件、滚动文件。设计的思想是基类落地类有一个纯虚log函数,具体的落地类继承基类并且重写基类log函数。最后通过简单工厂模式将这三种落地方式组织起来。

工厂模式:根据传入的不同落地类型,生产出指向基类的子类。

面临的问题:不同的落地方式可能会存在不同的参数、不同的类型

为了让工厂模式建造出相应的落地类,传入可变函数和模板参数。

落地的子类

往文件中落地:

设计思想是打开文件,但是如果频繁往文件中写数据就会频繁打开文件,关闭文件。

所以在构造的时候,就将文件的句柄保存

重写log函数,调用文件句柄写入数据。

按照大小滚动文件:
维护文件的当前size,文件最大size:一旦超过最大size就会创建新文件,往新文件写入数据

为了便于观察:在创建文件前会先获取文件名,文件名由当前时间+原子递增的序号组成

如 2022-10-10:8:50:20-1 创建新文件后,关闭旧文件,保存新文件的句柄

写入文件前先进行是否创建新文件的判断,再写入。

写入的时候,更新当前文件大小。

按照时间落地文件的设计思路:

  • 获取时间戳,对时间戳除当前的gap(如果gap是1minute就是60,1hour就是60*60)
  • 因为是按照时间间隔创建新文件,所以处于同一个文件时间除与gap必然是相同的。
  • 以时间落地文件的设计和以文件大小落地基本是一致的。


日志器设计(建造者模式)

当我们向通过日志系统来输出日志消息时,只需要创建日志器,调用日志器的debug、info,等接口输出我们想要的内容,同时输入的消息支持可变参数等等。就像printf()格式化的输出日志消息。

博主设计的日志系统支持

  • 同步落地(写数据由当前线程进行,可能会阻塞)。
  • 异步落地:异步写数据,只需要把数据交给缓冲区。

所谓的同步日志还是异步日志器都是基于普通日志器。因此先设计日志器Logger基类,继承出同步SyncLogger和AsyncLogger。

日志器的设计还必须包含消息的保存,利用格式化器对消息进行格式化,基于不同落地方式进行落地消息。这一系列的整合才能构建完整的日志器。所以日志器的创建是比较复杂的(用户并不知道、或者少创建了某个模块)借助建造者模式更加简洁优雅的构建日志器。

日志器的成员

  1. 日志器应该包含日志器名称
  2. 默认输出等级
  3. 互斥锁(设计多线程访问)
  4. 落地方式
  5. 格式化器

日志器的基类

抽象基类,抽象出debug、info、warning等接口,并且实现接口。接口的设置就是组织可变参数,调用格式化输出器转化成具体消息,至于落地方式,就交给派生类具体实现。

日志器支持多种落地方式,因此落地方式存储在数组中保存。将来具体的日志器类会重写log函数,根据同步或者异步,不同处理。

    class Logger;
    using LoggerPtr = std::shared_ptr<Logger>;
    class Logger
    {
    public:
        Logger(const std::string &logger_name, LogLevel::value level,
               const FormatterPtr &format, std::vector<LogSinkPtr> sinks)
            : _logger_name(logger_name), _format(format), _limit_level(level), _sinks(sinks.begin(), sinks.end())
        {
        }
   virtual void Dubug(const std::string &file, size_t line, const char *fmt, ...);
   virtual void Info(const std::string &file, size_t line, const char *fmt, ...);
   virtual void Warning(const std::string &file, size_t line, const char *fmt, ...);
   virtual void Fatal(const std::string &file, size_t line, const char *fmt, ...);
    protected:
        virtual void log(const char *data, size_t len) = 0;
        void serialize(LogLevel::value level, const std::string &file, size_t line, const char *data)
        {
            LogMsg lg(level, file, line, _logger_name, data);
            // 数据格式化,并放到流中
            std::stringstream ss;
            _format->format(ss, lg);
            log(ss.str().c_str(), ss.str().size());
        }

    protected:
        std::mutex _mutex;
        std::atomic<LogLevel::value> _limit_level;
        std::string _logger_name;
        std::vector<LogSinkPtr> _sinks; // 可能存在多种落地方式
        FormatterPtr _format;
    };

落地出同步日志器

同步互斥器就是当前线程进行写操作,设计多线程访问,需要加锁保护

    class SyncLogger : public Logger
    {
    public:
        SyncLogger(const std::string &logger_name, LogLevel::value level,
                   const FormatterPtr &formate, std::vector<LogSinkPtr> sinks)
            : Logger(logger_name, level, formate, sinks)
        {
        }
        void log(const char *data, size_t len) override
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if (_sinks.empty())
                return;
            for (auto &sink : _sinks)
            {
                sink->log(data, len);
            }
        }
    };

落地异步日志器

异步日志器就是将组织好的消息,将给内存缓冲区。就不管了。后续会详细介绍异步工作线程的原理。

    class AsyncLogger : public Logger
    {
    public:
        AsyncLogger(const std::string &logger_name, LogLevel::value level,
                    const FormatterPtr &formate, std::vector<LogSinkPtr> sinks, AsyncSafeType safe_type)
            : Logger(logger_name, level, formate, sinks)
        {
            _pasync = std::make_shared<AsyncLoop>(std::bind(&AsyncLogger::func, this, std::placeholders::_1), safe_type);
        }
        void log(const char *data, size_t len)
        {
            // std::unique_lock<std::mutex> lock(_mutex); push是线程安全的
            // 写数据往缓冲区放数据即可
            _pasync->Push(data, len);
        }

利用建造者模式创建日志器

建造者模式的一般步骤:抽象设备父类,具体设备子类,抽象零件父类,具体零件子类,如果涉及到顺序考虑用到指挥者构建。

抽象建造者的基类,建造日志器应该包含日志器名称、默认输出等级、格式化器、日志器类型(同步异步),落地方式

注意:

  1. 落地方式是通过工厂模式建造的,是一个模板可变参数。
  2. 对外提供build接口构建日志器。

   // 建造者模式
    // 抽象接口类
    // 派生出具体的接口类
    class LoggerBuilde
    {
    public:
        LoggerBuilde()
            : _type(LogType::SYNC), _limit_level(LogLevel::value::Dubug), _safe_type(AsyncSafeType::Async_Safe)
        {
        }
        void BuildType(LogType type) { _type = type; }
        void BuildName(std::string log_name) { _log_name = log_name; }
        void EnableUnSafe() { _safe_type = AsyncSafeType::Async_Unsafe; }
        void BuildLimit(LogLevel::value limit_level) { _limit_level = limit_level; }
        void BuildFormat(const std::string &foamat) { _formatter = std::make_shared<Formatter>(foamat); }
        template <typename SinkType, typename... Args>
        void BuildSink(Args &&...args)
        {
            LogSinkPtr psink = SinkFoctory::CreateSink<SinkType>(std::forward<Args>(args)...);
            _sinks.push_back(psink);
        }
        virtual LoggerPtr build() = 0;

    protected:
        AsyncSafeType _safe_type;
        LogType _type;
        std::string _log_name;
        LogLevel::value _limit_level;
        FormatterPtr _formatter;
        std::vector<LogSinkPtr> _sinks;
    };

 建造者模式基类

后续想派生出局部日志器,只要继承父类,重写build接口既可。


缓冲区的设计

一旦有数据需要落地时,当前线程阻塞进行IO写。这是非常消耗时间的事情,所以为了提高主线程的速度。实际的落地消息采用的是异步写。即由主线程只负责将数据格式化出来,组织好一条数据。实际上的文件打开和关闭和关闭交给子线程,不再阻塞当前线程。

待写数据是被放到主线程和子线程的共享位置(一块内存上)。常用的缓冲区是队列,考虑到STL的队列底层是链表,会频繁删节点和new节点,放弃了STL的队列。转而采用vector,实现缓冲区。

双缓冲区的由来:

线程往缓冲区上放数据,可能存在并发访问。线程和线程之间的竞争,需要加锁保护。同时生产者和消费者之间也会竞争缓冲区的资源。放数据和取数据也就需要加锁。频繁的申请锁和释放锁也是降低效率的原因。

因此采用双缓冲区。缓冲区分为写缓冲区和读缓冲区。写缓冲区由生产者所有,读缓冲区由消费者所有。只有当读缓冲区无数据并且写缓冲区有数据时,交换缓冲区才加锁保护。


设计思路

缓冲区是可读可写的,所以维护读指针和写指针。缓冲区的内容是一条格式化好的数据。

对外提供的接口

  • push( )生产者放数据---支持放多条消息
  • Front( )   获取一条头部消息
  • Writeable( )获取可写的空间
  • Readable( )获取可读的空间
  • moveRead() 移动读指针
  • moveWrite()移动写指针
  • 扩容
  • 缓冲区满
  • 缓冲区为空

要解决的问题:当缓冲区满了,如何处理?
1.阻塞,等待工作线程交换走写缓冲区  2.扩容

博主实现的缓冲区支持阻塞和扩容机制,阻塞机制没什么好说的,缓冲区满了就不写数据,返回fasle。阻塞交给上层来实现。

详细说一下扩容机制。

一般来说,实际的日志系统是不会用到扩容机制的,因为扩容是有风险的,一般扩二倍。会导致无限的申请内存。但是相对于极限测试,博主还是设计了扩容机制。

扩容的设计:

一定可用空间<len,就进行扩容。扩容分为快速扩和慢速扩。

  1. 快速扩容:每次扩原来空间的2倍。
  2. 慢速扩容:空间达到某个阈值的时候,扩一个增长量+lenth.

#define BUFFER_DEFAULT_SIZE 1024 * 1024 * 1     // 缓冲区起始默认10M
#define BUFFER_INCREAMENT_SIZE  1* 1024 * 1024   // 低速增长速度默认1M
#define BUFFER_THRESHOULD_VALUE 3 * 1024 * 1024 // 阈值默认40M  
namespace ns_logger
{
    class Buffer
    {
    public:
        Buffer(size_t capacity = BUFFER_DEFAULT_SIZE)
            : _buffer(capacity), _write_index(0), _read_idex(0) {}

        void Push(const char *data, size_t len)
        {
            // 扩容机制
            EnsureEnoughSpace(len);
            std::copy(data, data + len, &_buffer[_write_index]);
            _write_index += len;
        }

        void Pop(size_t len)
        {
            _read_idex += len;
            assert(_read_idex <= _write_index);
        }
        const char* GetFront() { return _buffer[_read_idex].c_str(); }
        bool Empty() { return _write_index == _read_idex; }
        // 可读的空间
        size_t Readable()
        {
            return _write_index - _read_idex;
        }
        size_t Writeable()
        {
            return _buffer.size() - _write_index;
        }
        void ReadMove(size_t len)
        {
            assert(_read_idex+len<=_write_index);
            _read_idex+=len;
        }
        void WriteMove(size_t len)
        {
            assert(_write_index+len<_buffer.size());
            _write_index+=len;
        }

        void Swap(Buffer &buffer)
        {
            _buffer.swap(buffer._buffer);
            std::swap(_read_idex, buffer._read_idex);
            std::swap(_write_index, buffer._write_index);
        }

        void ReSet(){
            _read_idex=_write_index=0;
        }

    private:
        void EnsureEnoughSpace(size_t len)
        {
            // 检查是否扩容
            if (len + _write_index < _buffer.size())
                return;
            // 扩容的大小
            size_t newsize;
            if (_buffer.size() <BUFFER_THRESHOULD_VALUE)
            {
                newsize = _buffer.size() * 2 + len;
            }
            // 低速扩容
            else
            {
                newsize = _buffer.size() + len + BUFFER_INCREAMENT_SIZE;
            }
            _buffer.resize(newsize);
        }

    private:
        std::vector<std::string> _buffer;
        size_t _read_idex;
        size_t _write_index;
    };
};

本篇主要介绍日志系统消息的落地简单工厂模式的使用、日志器的整合建造者模式的使用

另外了解异步工作机制,以及双缓冲区的设计。缓冲区的扩容机制

下一篇将进行异步工作器的设计。

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深度搜索

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值