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

news2024/12/22 16:55:22

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

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

消息落地的方式

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

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

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

消息落地设计思路

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2044249.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

打包时未添加camera模块,请参考https://ask.dcloud.net.cn/arss/1ooticle/283

今天在app打包使用的时候突然发现app在拍照上传照片的时候遇到这个问题 遇到这种情况通常是因为app打包的时候manifestjson文件中App模块配置中的Camera&Gallery配置没有打开&#xff0c;点击相应选项勾选即可 然后再上传打包就好了! 哈哈哈好久没写博客了最近太忙了&…

zigbee笔记:十三、议栈单播通信理论相关概念原理

一、端点&#xff08;Endpoint&#xff09; 1、端点基础知识 &#xff08;1&#xff09;、它是一个字节编号的&#xff08;端点编号是0-255&#xff09;&#xff0c;数据接收和发送的基本单元&#xff0c;在模块通信的时候&#xff0c;发送模块必须指定收发双方模块的网络地址和…

GNOME 如何关闭显示输出 ? (wayland / mutter / KMS / DRI) (源代码阅读)

GNOME 设置里面有这样一个功能: 鼠标/键盘无操作几分钟之后, 自动关闭显示输出, 具体表现为显示器黑屏, 进入休眠模式. 按一下鼠标/键盘, 恢复显示. 这是一个很常见的功能, 但是需要等待一段时间. 于是窝就想, 可不可以用一种简单的方式, 比如 执行一条命令, 随时随地直接进入这…

routine.hpp路由匹配模块

一.路由匹配模块介绍 路由匹配模块可以验证路由键&#xff08;routing key&#xff09;和绑定键&#xff08;binding key&#xff09;的合法性&#xff0c;并根据不同的交换机类型&#xff08;如Direct、Fanout和Topic&#xff09;进行消息的路由匹配。 二.Routine类的实现 设…

从〇 搭建PO模式的Web UI自动化测试框架

Page Object模式简介 核心思想 将页面元素和操作行为封装在独立的类中&#xff0c;形成页面对象&#xff08;Page Object&#xff09;。每个页面对象代表应用程序中的一个特定页面或组件。 优点&#xff1a; 代码复用性高 页面对象可以在多个测试用例中复用。 易于维护 …

10 个最佳 Java NLP 库和工具

发现用于高级自然语言处理的最佳 Java NLP 库。通过文本分析、情感分析等增强您的应用程序。 Java 已成为一种功能强大且用途广泛的编程语言&#xff0c;广泛用于开发跨领域的各种应用程序。其丰富的库和工具生态系统使其成为各种任务的理想选择&#xff0c;包括自然语言处理 (…

NVDLA专题1:NVDLA框架介绍

NVDLA概述 深度学习的计算部分主要可以分为4部分&#xff1a;卷积、激活单元&#xff08;神经元&#xff09;、池化和归一化。由于每个运算模块都有比较独特的共享特征&#xff0c;因此非常适合给每个模块设计一个对应的特殊硬件实现&#xff1a;内存访问模式容易预测并且很容…

超高速NVME FPGA存储卡记录

板卡概述 XNM-KU-M4 是一款基于KU115 的高速存储模块。 该模块基于NVME固态硬盘&#xff0c;主要用于高速实时数据流的存储和回放&#xff0c;主要用于雷达、通信、电子、卫星等领域&#xff0c;包括高速ADC数据采样实时记录、DAC数据回放、基于光纤或者Rapid IO的高速数据记录…

SOLIDWORKS 2024:开启创新设计新篇章

随着2024年的到来&#xff0c;SOLIDWORKS也迎来了全新的篇章——SOLIDWORKS 2024。这款由Dassault Systmes开发的三维CAD软件&#xff0c;一直以其强大的功能和易用性引领着工程设计领域的潮流。作为SOLIDWORKS在中国的官方授权代理商&#xff0c;亿达四方致力于为企业提供最新…

一个人活成一个团队:python的django项目devops实战

文章目录 一、需求规划二、代码管理三、创建流水线1、配置流水线源 四、自动测试五、自动构建六、自动部署七、总结 对于开发团队来说提高软件交付的速度和质量是一个永恒的话题&#xff0c;对于个人开发者来说同样如此。作为一个码农&#xff0c;一定会有几个自己私有的小项目…

漏洞扫描的重要性,如何做好漏洞扫描服务

随着互联网技术的飞速发展&#xff0c;网络安全问题已成为不容忽视的重大挑战。其中&#xff0c;系统漏洞威胁作为最常见且严重的安全危险之一&#xff0c;对组织和个人的信息资产构成了巨大威胁。下面我们就来了解下漏洞扫描的好处、漏洞扫描的操作方法以及如何做好网络安全。…

【学习笔记】A2X通信的协议(九)- 广播远程ID(BRID)

3GPP TS 24.577 V18.1.0的技术规范&#xff0c;主要定义了5G系统中A2X通信的协议方面&#xff0c;特别是在PC5接口和Uu接口上的A2X服务。以下是文件的核心内容分析&#xff1a; 7. 广播远程ID&#xff08;BRID&#xff09; 7.1 概述 本条款描述了以下程序&#xff1a; 在用…

复现、并改进open-mmlab的mmpose详细细节

复现open-mmlab的mmpose详细细节 1.配置环境2.数据处理3.训练4.改进mmpose4.1 快速调试技巧4.2 快速定位4.3 改进backbone4.3.1 使用说明4.3.2 改进案例4.3.2.1 复现mmpose原配置文件4.3.2.2 复现开源项目4.3.2.3 修改配置文件4.3.2.4 修改新模型 4.4 添加auxiliary_head4.4.1 …

Python OpenCV 影像处理:读取、显示、储存影片

► 前言 本篇将介绍使用OpenCV Python撷取网路摄影机(webcam)的即时画面影像处理与显示&#xff0c;以及透过读取、显示和储存硬盘中的影片档案来实现影片操作。这将帮助大家了解如何使用OpenCV在影片上进行各种操作。 ► OpenCV Python撷取网路摄影机 OpenCV首先建立了一个…

【计算机网络】TCP实战

其实有了UDP的基础&#xff0c;TCP不管怎么说学习起来都还是比较舒服的&#xff0c;至少是比直接就学习TCP的感觉好。 这篇文章最多就是介绍一下起手式&#xff0c;如果想带业务的话和UDP那篇是完全一样的&#xff0c;就不进行演示了。 总的来说还是很简单的。 目录 Echo服务端…

魔方远程时时获取短信内容APP 前端Vue 后端Ruoyi框架(含搭建教程)

前端Vue 后端Ruoyi框架 APP原生JAVA 全兼容至Android14(鸿蒙 澎湃等等) 前后端功能&#xff1a; ①后端可查看用户在线状态(归属地IP) ②发送短信(自定义输入收信号码以及短信内容&#xff0c;带发送记录) ③短信内容分类清晰(接收时间、上传时间等等) ④前后端分离以及A…

Doris与StarRocks

目录 Doris Doris 架构 存储引擎 查询引擎 索引结构 存储模型 物化视图 使用场景 StarRocks 架构设计 架构选择 存算一体 节点 FE BE 存算分离 节点 存储 缓存 适用场景 OLAP 多维分析 实时数据仓库 高并发查询 统一分析 Doris和StarRocks对比 大规模…

Vue3中组件的多种写法

SFC单文件组件&#xff0c;一个vue写一个组件 使用 defineComponent h函数 去进行组件编写 使用 defineComponent JSX/TSX 去进行组件编写 需要安装插件pnpm i vitejs/plugin-vue-jsx -D 引入 配置 使用组件

Android的OkHttp使用和原理

前言 OkHttp的出现代替了HttpUrlConnection&#xff0c;被谷歌官方收纳为底层的网络框架。特点如下&#xff1a; 支持HTTP/2框架下的socket复用通过连接池减少连接的延时使用GZIP进行数据压缩使用缓存技术避免重复请求 当网络出现问题时&#xff0c;OkHttp会静默重新恢复连接…

uniapp组件使用

uni-popup 默认z-index是99 https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html#uni-popup-%E5%BC%B9%E5%87%BA%E5%B1%82 uni-icons uniapp自带图标&#xff1a;https://hellouniapp.dcloud.net.cn/pages/extUI/icons/icons <uni-icons type"left"…