C++项目实战——基于多设计模式下的同步异步日志系统-⑩-异步缓冲区类与异步工作器类设计

news2024/11/25 20:43:50

文章目录

  • 专栏导读
  • 异步缓冲区设计思想
  • 异步缓冲区类设计
  • 异步工作器类设计
  • 异步日志器设计

专栏导读

🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。

🌸专栏简介:本文收录于 C++项目——基于多设计模式下的同步与异步日志系统

🌸相关专栏推荐:C语言初阶系列C语言进阶系列C++系列数据结构与算法Linux

在这里插入图片描述
为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响其效率(例如由于网络原因导致日志写入阻塞,进而导致业务线程阻塞),因此我们需要设计一个异步日志器

异步的思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区(一块指定内存)当中,接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际落地操作)

所以,异步日志器的实现思想:

  • 设计一个线程安全的缓冲区
  • 创建一个异步工作线程,专门负责缓冲区中日志消息落地操作。

异步缓冲区设计思想

在任务池的设计中,有很多备选方案,比如队列、循环队列等,但是不管哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间具有互斥关系,因此每一次任务的添加与取出都有可能涉及锁的冲突

所以我们采用双缓冲区的的设计思想,优势在于:

  • 避免了空间的频繁申请与释放,且尽可能的减少了生产者与消费则之间锁冲突的概率,提高了任务处理效率

双缓冲区的设计思想是:采用两个缓冲区,一个用来进行任务写入(push pool),一个进行任务处理(pop pool)。当异步工作线程(消费者)将缓冲区中的数据全部处理完毕之后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程写入也会产生冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了消费者与生产者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的的消耗。

在这里插入图片描述

异步缓冲区类设计

类中包含的成员:

  • 一个存放字符串数据的缓冲区(使用vector进行空间管理);
  • 当前写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖);
  • 当前读取数据位置的指针(指向可读区域的起始位置,当读取指针与写入指针指向相同的位置表示数据读取完了);

类中提供的操作:

  • 向缓冲区中写入数据
  • 获取可读数据起始地址的接口
  • 获取可读数据长度的接口
  • 移动读写位置的接口
  • 初始化缓冲区的操作(将读写位置初始化–在一个缓冲区所有数据处理完毕之后);
  • 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)。

注意,缓冲区中直接存放格式化后的日志消息字符串,而不是LogMsg对象,这样做有两个好处:

  • 减少了LogMsg对象频繁的构造的消耗;
  • 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率。
#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
#include "util.hpp"
#include <vector>
#include <cassert>

namespace LOG
{
    #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), _writer_idx(0), _reader_idx(0) {}
        // 向缓冲区中写入数据
        void push(const char* data, size_t len)
        {
            // 1.考虑空间不够则扩容
            ensureEnoughSize(len);
            // 2.将数据拷贝到缓冲区
            std::copy(data, data + len, &_buffer[_writer_idx]);
            // 3.将当前写入位置向后偏移
            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 reset()
        {
            _writer_idx = 0;
            _reader_idx = 0;
        }
        // 对buffer实现交换操作
        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 + len; // 小于阈值则翻倍增长
            }
            else
            {
                new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len;
            }
            _buffer.resize(new_size);
        }
        // 对读指针进行向后偏移操作
        void moveReader(size_t len)
        {
            assert(len <= readAbleSize());
            _reader_idx += len;
        }
        // 对写指针进行向后偏移操作
        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; // 当前可写数据的指针
    };
}
#endif

异步工作器类设计

异步工作器的主要任务是,对缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区

异步工作器类管理的成员有:

  • 双缓冲区(生产,消费);
  • 互斥锁:保证线程安全;
  • 条件变量-生产&消费:生产缓冲区中没有数据,处理完消费缓冲区数据后就休眠;
  • 回调函数:针对缓冲区中数据的处理接口——外界传入一个函数,告诉异步日志器该如何处理。

异步工作器类提供的操作有:

  • 停止异步工作器
  • 添加数据到缓冲区

私有操作:

  • 创建线程
  • 线程入口函数:在线程入口函数中交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换;
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp"
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>

namespace LOG
{
    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 looper_type = AsyncType::ASYNC_SAFE)
            :_looper_type(looper_type),
            _stop(false),
            _call_back(cb),
            _thread(std::thread(&AsyncLooper::threadEntry, this))
        {}

        ~AsyncLooper() { stop(); }

        void stop() 
        {
            _stop = true; // 将退出标志设置为true
            _con_cond.notify_all(); // 唤醒所有工作线程
            _thread.join(); // 等待工作线程退出
        }
        void push(const char* data, size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            // 条件变量控制,若缓冲区剩余空间大小等于数据长度,则可以添加数据
            if(_looper_type == AsyncType::ASYNC_SAFE)
                _pro_cond.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
            // 能够走下来说明条件满足,可以向缓冲区添加数据了
            _pro_buf.push(data, len);
            // 唤醒消费者对缓冲区中的数据进行处理
            _con_cond.notify_one();
        }
    private:
        // 线程入口函数 -- 对消费者缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
        void threadEntry()
        {
            // 为互斥锁设置一个声明周期,当缓冲区交换完毕就解锁
            while(1)
            {
                {
                    // 1.判断生产缓冲区有没有数据,有则交换,无则阻塞
                    std::unique_lock<std::mutex> lock(_mutex);
                    if(_stop && _pro_buf.empty()) break;
                    // 若当前是退出前被唤醒或者是有数据被唤醒,则返回真,继续向下运行,否则重新进入休眠
                    _con_cond.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
                    _con_buf.swap(_pro_buf);
                    // 2.唤醒生产者
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                        _pro_cond.notify_all();
                }
                // 3.被唤醒后,对消费者缓冲区进行数据处理
                _call_back(_con_buf);
                // 4.初始化消费者缓冲区
                _con_buf.reset();
            }
        }
    private:
        Functor _call_back;
    private:
        AsyncType _looper_type;
        std::atomic<bool> _stop;
        Buffer _pro_buf;
        Buffer _con_buf;
        std::mutex _mutex;
        std::condition_variable _pro_cond;
        std::condition_variable _con_cond;
        std::thread _thread; // 异步工作器对应的工作线程
    };
}
#endif

异步日志器设计

异步日志器继承自日志器类,并在同步日志器类上拓展了异步工作器。当我们需要异步输出日志的时候,需要创建异步日志器和消息处理器,调用异步日志器的log、debug、error、info、fatal等函数输出不同级别日志。

  • log函数为重写Logger类的函数,主要实现将日志日志数据加入异步缓冲区;
  • realLog函数主要由异步线程调用(是为异步工作器设置的回调函数),完成日志的实际落地操作。
class AsyncLogger : public Logger
{
public:
    AsyncLogger(const std::string &logger_name,
                LogLevel::value level,
                LOG::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 (_sinks.empty())
            return;
        for (auto &sink : _sinks)
        {
            sink->log(buf.begin(), buf.readAbleSize());
        }
    }

private:
    AsyncLooper::ptr _looper; // 异步工作器
};

在这里插入图片描述

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

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

相关文章

数据结构----算法--排序算法

数据结构----算法–排序算法 一.冒泡排序&#xff08;BubbleSort&#xff09; 1.冒泡排序的核心思想 相邻两个元素进行大小比较&#xff0c;如果前一个比后一个大&#xff0c;就交换 注意&#xff1a; 在冒泡排序的过程中&#xff0c;促进了大的数往后去&#xff0c;小的数…

2023年中国功率半导体分立器件发展趋势分析:产品向高效率、低成本发展[图]

功率半导体分立器件是指被规定完成某种基本功能&#xff0c;并且本身在功能上不能再细分的半导体器件。功率半导体分立器件的应用几乎覆盖所有的电子制造业&#xff0c;传统应用领域包括消费电子、网络通讯等&#xff1b;近年来&#xff0c;汽车电子及充电系统、新能源发电等领…

log4j2同步日志引发的性能问题 | 京东物流技术团队

1 问题回顾 1.1 问题描述 在项目的性能测试中&#xff0c;相关的接口的随着并发数增加&#xff0c;接口的响应时间变长&#xff0c;接口吞吐不再增长&#xff0c;应用的CPU使用率较高。 1.2 分析思路 谁导致的CPU较高&#xff0c;阻塞接口TPS的增长&#xff1f;接口的响应时…

零基础实战部署云服务器项目-服务器部署(window server2012)

目录 什么是服务器呢&#xff1f; 项目服务器运行环境 一、申请服务器 1.找店铺 2.1选配置 型号 2.2配置&#xff08;具体配置&#xff09; 3.下单 4.收货 情况一&#xff1a;弹窗有进入控制台 情况二&#xff1a;手快关闭了窗口 所以下一步我们远程连接 方法二&#xff1a;win…

实验室安全巡检管理系统—全面安全检查

安全巡检管理系统可以代替传统的线下纸质巡查&#xff0c;微信扫码进行巡检记录、隐患上报、设备保养维护等工作&#xff0c;管理人员可远程实时查看巡查情况&#xff0c;同时在巡查结束后对现场问题进行闭环整改管理。大大提高现场巡查效率&#xff0c;辅助用户对巡查工作实时…

代码随想录二刷 Day 35

122.买卖股票的最佳时机 II 数组两两求差&#xff0c;然后把正数加起来 class Solution { public:int maxProfit(vector<int>& prices) {int sum0;int diff0;for(int i0;i<prices.size()-1;i){diff prices[i1] - prices[i];if(diff>0){sumdiff;}} return su…

智慧公厕设计选哪家?智慧公厕厂家创新性智慧公厕建设方案揭秘

随着人们生活水平的提高&#xff0c;公共卫生设施的建设也变得越来越重要。其中&#xff0c;智慧公厕作为一种创新的建设方案&#xff0c;备受关注。那么&#xff0c;在众多智慧公厕厂家中&#xff0c;广州中期科技有限公司的智慧公厕怎么建设&#xff1f;其中的技术方案有何独…

【广州华锐互动】利用VR开展高压电缆运维实训,提供更加真实、安全的学习环境

VR高压电缆维护实训系统由广州华锐互动开发&#xff0c;应用于多家供电企业的员工培训中&#xff0c;该系统突破了传统培训的限制&#xff0c;为学员提供了更加真实、安全的学习环境&#xff0c;提高了培训效率和效果。 传统电缆井下运维培训通常是在实际井下环境中进行&#x…

Protocols/面向协议编程, DependencyInjection/依赖式注入 的使用

1. Protocols 定义实现协议&#xff0c;面向协议编码 1.1 创建面向协议实例 ProtocolsBootcamp.swift import SwiftUI/// 颜色样式协议 protocol ColorThemeProtocol {var primary: Color { get }var secondary: Color { get }var tertiary: Color { get } }struct DefaultCol…

MySQL JSON_TABLE() 函数

JSON_TABLE()函数从一个指定的JSON文档中提取数据并返回一个具有指定列的关系表。 应用&#xff1a;数据库字段以JSON 存储后&#xff0c;实际应用需要对其中一个字段进行查询 语法 JSON_TABLE(json,path COLUMNS(column[,column[,...]]))column:name参数 json必需的。一个 …

2023年全球半导体零部件市场发展现状分析:半导体零部件行业集中度高[图]

半导体零部件指的是在材料、结构、工艺、精度和品质、稳定性及可靠性等性能方面符合半导体设备技术要求的零部件&#xff0c;如O-Ring密封圈、精密轴承、射频电源、静电吸盘&#xff08;ESC&#xff09;、MFC流量计、石英件、硅及碳化硅件等。 半导体零部件分类 资料来源&…

【JVM面试】从JDK7 到 JDK8, JVM为啥用元空间替换永久代?

系列文章目录 【JVM系列】第一章 运行时数据区 【面试】第二章 从JDK7 到 JDK8, JVM为啥用元空间替换永久代&#xff1f; 大家好&#xff0c;我是青花。拥有多项发明专利&#xff08;都是关于商品、广告等推荐产品&#xff09;。对广告、Web全栈以及Java生态微服务拥有自己独到…

【京东开源项目】微前端框架MicroApp 1.0正式发布

介绍 MicroApp是由京东前端团队推出的一款微前端框架&#xff0c;它从组件化的思维&#xff0c;基于类WebComponent进行微前端的渲染&#xff0c;旨在降低上手难度、提升工作效率。MicroApp无关技术栈&#xff0c;也不和业务绑定&#xff0c;可以用于任何前端框架。 源码地址…

如何优雅的实现接口统一调用

耦合问题 有些时候我们在进行接口调用的时候&#xff0c;比如说一个push推送接口&#xff0c;有可能会涉及到不同渠道的推送&#xff0c;以我目前业务场景为例&#xff0c;我做结算后端服务的&#xff0c;会与金蝶财务系统进行交互&#xff0c;那么我结算后端会涉及到多个结算…

第二证券:国际油价大幅上涨 后市恐难持续走高

上个买卖周&#xff0c;受巴以冲突影响&#xff0c;原油商场成为各方关注的焦点。到上星期五收盘&#xff0c;布伦特原油周内涨幅达7%以上&#xff0c;为本年2月以来最大周涨幅&#xff0c;WTI原油周内累计上涨近6%。业内人士认为&#xff0c;其时地缘要素是导致油价出现异动的…

3分钟,快速上手Postman接口测试!

Postman是一个用于调试HTTP请求的工具&#xff0c;它提供了友好的界面帮助分析、构造HTTP请求&#xff0c;并分析响应数据。实际工作中&#xff0c;开发和测试基本上都有使用Postman来进行接口调试工作。有一些其他流程的工具&#xff0c;也是模仿的Postman的风格进行接口测试工…

DDD之上下文映射图(Context Mapping)

领域驱动设计系列文章&#xff0c;点击上方合集↑ 1. 开头 在DDD中&#xff0c;限界上下文与限界上下文之间需要相互集成&#xff0c;这种集成关系在DDD中称为上下文映射&#xff08;Context Mapping&#xff09;&#xff0c;也就是子域与子域之间的集成关系。 所以首先我们…

2023年中国微创手术器械及耗材发展趋势分析:发展呈现集成化、智能化[图]

随着微创外科手术渗透率的提升&#xff0c;国内微创外科手术器械及配件&#xff08;MISIA&#xff09;市场迎来爆发&#xff0c;预计2023年微创手术器械及耗材&#xff08;MISIA&#xff09;市场规模将达到238亿元&#xff0c;其中一次性MISIA市场规模约为204亿元&#xff0c;可…

网络安全——自学(黑客技术)

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学&#xff1f;如何学&#xff1f; 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c…

【OpenVINO】OpenVINO C# API 常用 API 详解与演示

OpenVINO C# API 常用 API 详解与演示 1 安装OpenVINO C# API2 导入程序集 3 初始化OpenVINO 运行时内核4 加载并获取模型信息4.1 加载模型4.2 获取模型信息 5 编译模型并创建推理请求6 张量Tensor6.1 张量的获取与设置6.2 张量的信息获取与设置 7 加载推理数据7.1 获取输入张量…