[C++]——同步异步日志系统(8)

一、项目目录结构

example:如何使用项目的具体样例
extend:扩展代码
logs:项目的各个模块,项目源码
pratice:练习代码,项目前置学习的代码
bench:用来进行性能测试
整理完成后,目录结构如下(注意:扩展代码和具体样例代码一定要进行测试)

在这里插入图片描述

一、功能测试

// 测试代码
#include "../logs/logslearn.h"

//进行功能测试
void test_log(const std::string &name){
   INFO( "%s", "测试开始");
    logslearn::Logger::ptr logger=logslearn::LoggerManager::getInstance().getLogger(name);
    //测试日志打印
    logger->debug( "%s", "测试日志");
    logger->info( "%s", "测试日志");
    logger->warn( "%s", "测试日志");
    logger->error( "%s", "测试日志");
    logger->fatal("%s", "测试日志");
    INFO( "%s", "测试结束");
}
int main()
{
    std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::GlobalLoggerBuilder());
    //建造者构建零部件
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(logslearn::loglevel::value::DEBUG);
    builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%p]%m%n");
    builder->buildLoggerType(logslearn::LoggerType::LOGGER_SYNC);
    builder->buildSink<logslearn::StdoutSink>();                                 // 标准输出落地
    builder->buildSink<logslearn::FileSink>("./logfile/async.log"); // 文件落地方式
    builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
    builder->build();
    
    test_log("async_logger");

    return 0;
}

打印结果:异步方式
在这里插入图片描述
打印结果:同步方式
在这里插入图片描述

测试⼀个⽇志器中包含有所有的落地⽅向,观察是否每个⽅向都正常落地,分别测试同步⽅式和异步⽅式落地后数据是否正常。(我们的代码进行功能测试都很正常)

二、性能测试

下⾯对⽇志系统做⼀个性能测试,测试⼀下平均每秒能打印多少条⽇志消息到⽂件。

测试三要素:

1.测试环境
2.测试方法
3.测试结果

测试工具的编写:

1.可以控制写日志线程数量
2.可以控制写日志的总数量
分别对于同步日志器 & 异步日志器进行各自的性能测试,
需要测试单写日志线程的性能 需要测试多写日志线程的性能

实现:

封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小 在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束。
所耗时间=结束时间-起始时间
每秒输出量 =日志数量/总耗时
每秒输出大小 =日志数量*单条日志大小/总耗时
注意:异步日志输出这里,我们启动非安全式,纯内存写入(不去考虑实际落地的时间)

测试环境:

CPU:Intel® Core™ i5-9300HF CPU @ 2.40GHz
RAM:只读存储器 16.0 GB
ROM:随机存取存储器 327GB—SSD
OS:ubuntu-20.04

2.1 项目性能测试工具实现

  1. 在对项目测试之前,需要编写测试工具,测试工具的具体实现放到bench里。
#include "../logs/logslearn.h"
// c++11提供的时间特性
#include <chrono>
// 设计性能测试功能
// logger_name日志器的名字,thr_count线程数的个数,msg_counr日志消息的总条数,len日志消息的长度
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
    // 1.获取日志器
    logslearn::Logger::ptr logger = logslearn::getLogger(logger_name);
    // 如果没找到日志器就返回空
    if (logger.get() == nullptr)
    {
        return;
    }
    // 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;
    // 创建指定数量的线程,push_back()构造然后拷贝,插入元素到末尾,emplace_back()构造并插入元素到末尾
    // 打印测试日志总条数,总大小
    std::cout << "\t测试日志:" << msg_count << "条,\t总大小:" << (msg_count * msg_len) / 1024 << "KB\n";
    for (int i = 0; i < thr_count; i++)
    {
        // 插入元素时用lambad表达式
        threads.emplace_back([&, i]()
                             {
            // 4.线程函数内部开始计时,高精度获得当前的系统时间
            auto start=std::chrono::high_resolution_clock::now();
            // 5.开始循环写日志
            for(int i=0;i<msg_per_thr;i++){
            //打印日志
            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";
}

2. 项目性能测试

因为我们做的是性能测试,所以每一次只测试一种结果。
同步日志器性能受磁盘影响、

1. 同步日志器单线程

// 测试同步日志器
void sync_bench()
{
    // 创建一个同步日志器建造者
    std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::GlobalLoggerBuilder());
    // 建造者构建零部件
    builder->buildLoggerName("sync_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(logslearn::LoggerType::LOGGER_SYNC);
    builder->buildSink<logslearn::FileSink>("./logfile/sync.log"); // 文件落地方式
    // builder->buildSink<logslearn::StdoutSink>();                                   // 标准输出落地
    // builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
    builder->build();
    //  测试单线程情况
    bench("sync_logger", 1, 1000000, 100);
}
int main()
{	// 同步日志器单线程
    sync_bench();
    return 0;
}

测试结果:
在这里插入图片描述
2. 同步日志器多线程

// 测试同步日志器
void sync_bench()
{
    // 创建一个同步日志器建造者
    std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::GlobalLoggerBuilder());
    // 建造者构建零部件
    builder->buildLoggerName("sync_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(logslearn::LoggerType::LOGGER_SYNC);
    builder->buildSink<logslearn::FileSink>("./logfile/sync.log"); // 文件落地方式
    // builder->buildSink<logslearn::StdoutSink>();                                   // 标准输出落地
    // builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
    builder->build();
     bench("sync_logger",3,1000000,100);
}
int main()
{
    sync_bench();
    return 0;
}

在这里插入图片描述
为什么多线程比单线程耗时慢?
单线程没有锁冲突,他是一个加锁写日志,加锁写日志过程,是一个串行接口
因为多线程写日志是多个线程加锁写日志的过程,会存在锁冲突问题。
我的电脑磁盘性能比较好,如果是更大量的日志,就是单线程耗时慢
比如日志达到2000000条,结果如下:
在这里插入图片描述
3. 异步日志器单线程

本质:多线程和单线程都是向内存写数据要考虑cpu和内存的性能,不去考虑磁盘落地的情况。

// 测试异步日志器
void async_bench()
{
// 创建一个同步日志器建造者
    std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::GlobalLoggerBuilder());
    // 建造者构建零部件
    builder->buildLoggerName("async_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(logslearn::LoggerType::LOGGER_ASYNC);
    builder->buildEnabeUnSafeAsync();//开启非安全模式————主要是为了将实际落地时间排除在外
    builder->buildSink<logslearn::FileSink>("./logfile/async.log"); // 文件落地方式
    // builder->buildSink<logslearn::StdoutSink>();                                   // 标准输出落地
    // builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
    builder->build();
    //  测试单线程情况
    bench("async_logger", 1, 1000000, 100);

}
int main()
{
    async_bench();
    return 0;
}

测试:
在这里插入图片描述

4. 异步日志器多线程

// 测试异步日志器
void async_bench()
{
// 创建一个同步日志器建造者
    std::unique_ptr<logslearn::LoggerBuilder> builder(new logslearn::GlobalLoggerBuilder());
    // 建造者构建零部件
    builder->buildLoggerName("async_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(logslearn::LoggerType::LOGGER_ASYNC);
    builder->buildEnabeUnSafeAsync();//开启非安全模式————主要是为了将实际落地时间排除在外
    builder->buildSink<logslearn::FileSink>("./logfile/async.log"); // 文件落地方式
    // builder->buildSink<logslearn::StdoutSink>();                                   // 标准输出落地
    // builder->buildSink<logslearn::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
    builder->build();
     // 测试多线程情况
    bench("async_logger",3,1000000,100);
}
int main()
{
    async_bench();
    return 0;
}

测试:
在这里插入图片描述
我们把日志的数量提高到2000000条
结果如下:
在这里插入图片描述
cpu和内存的性能越好打印日志越快,日志的多少和线程数无关(不会因为落地而阻塞)。

总结:

能够通过上边的测试看出来,⼀些情况:
在单线程情况下,异步效率看起来还没有同步⾼,这是因为现在的IO操作,用户态都是从缓冲区进入到缓冲区,因此我们当前测试⽤例看起来的同步其实⼤多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作,⽽异步单线程效率看起来低,也有⼀个很重要的原因就是单线程同步操作中不存在锁冲突,⽽单线程异步⽇志操作存在⼤量的锁冲突,因此性能也会有⼀定的降低。 但是,我们也要看到限制同步⽇志效率的最⼤原因是磁盘性能,打⽇志的线程多少并⽆明显区别,线程多了反⽽会降低,因为增加了磁盘的读写争抢,⽽对于异步⽇志的限制,并⾮磁盘的性能,⽽是cpu和内存的处理性能,打⽇志并不会因为落地⽽阻塞,因此在多线程打⽇志的情况下性能有了显著的提⾼。

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
桥接模式是一种结构型设计模式,它将抽象和实现分离,使它们可以独立地变化。桥接模式的核心思想是将一个大类或一组类分解成抽象和实现两个独立的维度,使它们可以独立地变化和扩展,同时通过桥接来将它们连接起来。 在C++中,桥接模式通常通过虚函数实现。抽象部分通过基类定义接口,而实现部分通过派生类实现具体的功能。通过将抽象部分的指针作为参数传递给实现部分的函数,就可以实现两个部分的连接。 下面是一个简单的桥接模式的C++示例: ```c++ class Implementor { public: virtual void operation() = 0; virtual ~Implementor() {} }; class ConcreteImplementorA : public Implementor { public: void operation() override { // 具体的实现A } }; class ConcreteImplementorB : public Implementor { public: void operation() override { // 具体的实现B } }; class Abstraction { public: Abstraction(Implementor* implementor) : m_implementor(implementor) {} virtual void operation() = 0; virtual ~Abstraction() {} protected: Implementor* m_implementor; }; class RefinedAbstraction : public Abstraction { public: RefinedAbstraction(Implementor* implementor) : Abstraction(implementor) {} void operation() override { m_implementor->operation(); // 其他操作 } }; int main() { Implementor* implementorA = new ConcreteImplementorA(); Implementor* implementorB = new ConcreteImplementorB(); Abstraction* abstractionA = new RefinedAbstraction(implementorA); Abstraction* abstractionB = new RefinedAbstraction(implementorB); abstractionA->operation(); abstractionB->operation(); delete abstractionA; delete abstractionB; delete implementorA; delete implementorB; return 0; } ``` 在上面的示例中,Implementor是实现部分的抽象基类,ConcreteImplementorA和ConcreteImplementorB是具体的实现类。Abstraction是抽象部分的基类,RefinedAbstraction是抽象部分的具体实现类。在main函数中,我们创建了不同的Implementor和Abstraction对象,并通过它们来完成不同的操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值