首页 > 其他分享 >【项目】多设计模式下的同步&&异步日志系统(二)

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

时间:2024-08-18 17:52:09浏览次数:13  
标签:std 异步 const 落地 文件 && 缓冲区 日志 设计模式

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

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

消息落地的方式

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

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

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

消息落地设计思路

落地类支持扩展,目前支持落地到标准输出、文件、滚动文件。设计的思想是基类落地类有一个纯虚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;
    };
};

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

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

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

标签:std,异步,const,落地,文件,&&,缓冲区,日志,设计模式
From: https://blog.csdn.net/m0_73299809/article/details/141201456

相关文章

  • 【项目】多设计模式下的同步&&异步日志系统(三)
    继前俩次完成了日志系统的等级类、消息结构以及格式化消息,并且将格式化的数据实现落地。落地存在同步和异步的落地方式。同步:本线程生成消息,并且进行IO写。异步:线程生成消息,交给子线程写。为此实现了双缓冲区用来减少异步带来的频繁申请锁释放锁减低效率。本文继续实现异步......
  • 026、Vue3+TypeScript基础,使用async和await来异步读取axios的网络图片
    01、App.vue代码如下:<template><divclass="app"><h2>App.Vue</h2><Person/></div></template><scriptlang="ts"setupname="App">//JS或TSimportPersonfrom'./......
  • 设计模式六大原则之:开闭原则
    1.开闭原则简介开闭原则(OpenClosedPrinciple,OCP)‌是面向对象程序设计(OOP)中的一个基本原则,也是软件工程中的一项重要原则。它的核心思想是:一个软件实体(如类、模块或函数)应该对扩展开放,即当需求变化时,可以通过添加新的代码进行扩展来满足新的需求,而不需要修改现有的代码。......
  • 高级java每日一道面试题-2024年8月16日-设计模式篇-解释装饰者模式和代理模式的区别?
    如果有遗漏,评论区告诉我进行补充面试官:解释装饰者模式和代理模式的区别?我回答:在Java中,装饰者模式(DecoratorPattern)和代理模式(ProxyPattern)都是常用的设计模式,它们在结构上看起来有些相似,但实际上它们的目的、应用场景和实现方式存在明显的区别。下面详细解释这两种......
  • LabVIEW异步同步模式
    LabVIEW 的异步和同步模式在数据流编程和任务执行方面有不同的应用场景。以下是对这两种模式的详细介绍和比较。1. 同步模式同步模式指的是任务按照一定的顺序依次执行,前一个任务必须完成后,后一个任务才能开始。具体来说,在 LabVIEW 中,如果一个 VI(虚拟仪器)调用另一个 ......
  • 直播系统源码,异步加载UI相关知识点
    简述FutureBuilder和StreamBuilder都是Flutter为开发者提供快速实现异步加载UI的方案,它们不同的是一个依赖的是Future另一个依赖的是Stream,一般情况下我们使用Future就可以完成大部分的异步操作,它可以异步加载我们需要的数据,那Stream是不是多余的呢?并不是,Stream专门用于异步加......
  • Golang使用Option设计模式优雅处理可选参数
    go语言不像其他语言函数的参数可以设置默认值以下是参考第三方库的写法packagemainimport"fmt"typeUserstruct{namestringageintidint}//Option代表可选参数typeOptionfunc(foo*User)//WithName为name字段提供一个设置器funcWithName(name......
  • 设计模式---构建者模式(Builder Pattern)
    构建者模式(BuilderPattern)是一种创建型设计模式,旨在将复杂对象的构建过程与其表示分离。它允许使用相同的构建过程创建不同的表示。该模式通常用于构建复杂对象,这些对象由多个部分组成或具有多个可选属性。构建者模式的核心要素:Builder(构建者):定义构建对象的接口,声明创建部......
  • 【通信理论知识】数据传送的方式:串/并行;传输方向:单工、半/全双工;传输方式:同步/异步
    串行/并行通信按数据传送的方式,通讯可分为串行通讯与并行通讯。串行通讯就像单个车道的公路,同一时刻只能传输一个数据位的数据。并行通讯就像多个车道的公路,可以同时传输多个数据位的数据。传输方向(单工、半/全双工)全双工和半双工通信的本质区别(SPI、IIC)半双工......
  • 【Java Lambda系列】新玩法,用Lambda重构设计模式
    前言前面三章通过理论+案例的方式对Lambda的描述,应该能基本上解决大家日常开发中所遇到的Lambda问题,为了更好的展现Lambda魅力,和加深巩固Lambda知识点,今天咱们讨论Lambda如何重构设计模式!关于设计模式众所周知,设计模式是一群大佬程序员将对程序设计的经验归纳总结起来的......