[项目] C++基于多设计模式下的同步&异步日志系统

[项目] C++基于多设计模式下的同步&异步日志系统

文章目录

    • [项目] C++基于多设计模式下的同步&异步日志系统
      • 日志系统
      • 1、项目介绍
      • 2、开发环境
      • 3、核心技术
      • 4、日志系统介绍
        • 4.1 日志系统的价值
        • 4.2 日志系统技术实现
          • 4.2.1 同步写日志
          • 4.2.2 异步写日志
      • 5、相关技术知识
        • 5.1 不定参函数
          • 5.1.1 不定参宏函数
        • 5.2 设计模式
          • 5.2.1 六大原则
          • 5.2.2 单例模式
          • 5.2.3 工厂模式
          • 5.2.4 建造者模式
          • 5.2.5 代理模式
      • 日志系统框架设计
        • 6.1 模块划分
        • 6.2 模块关系图
      • 7.代码实现
        • 7.1 实用工具类
        • 7.2 日志等级类
        • 7.3 日志消息类
        • 7.4 日志输出格式化类
        • 7.5 日志落地类(工厂模式)
        • 7.6 日志器类(建造者模式)
        • 7.7 异步日志双缓冲区类
        • 7.8 异步工作器类
        • 7.9 单例日志器管理类
        • 7.10 日志宏&全局接口设计
      • 8.功能测试
      • 9.性能测试
      • 10.扩展

日志系统

日志:程序运行过程中记录的程序运行状态信息

作用:记录了程序运行状态信息,便于程序员能够随时根据状态信息,对系统程序的运行状态进行分析。能够非常简便的进行详细的日志输出以及控制

1、项目介绍

本项目主要实现的是一个日志系统,其支持以下功能:

  • 支持多级别日志信息
  • 支持同步日志信息和异步输出日志
  • 支持可靠写入日志到控制台、文件、滚动文件中
  • 支持多线程程序并发写日志
  • 支持扩展不同的日志落地

2、开发环境

  • 操作系统:Ubuntu 20.04
  • 编辑器:vscode + vim
  • 编译器/调试器:g++/ gdb
  • 项目自动化构建工具:Makefile

3、核心技术

  • 类层次化设计(继承、多态的实际应用)
  • C++11新特性(多线程库、智能指针、右值引用等)
  • 双缓冲区设计思想
  • 生产者消费者模型
  • 设计模式(单例、工厂、代理、建造者等)

4、日志系统介绍

4.1 日志系统的价值
  • 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的,可以借助日志系统来打印一些日志来帮助开发人员解决问题
  • 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行分析
  • 对于一些高频操作(如定时器、心跳包等)在少量调试下可能无法触发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题
  • 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug
  • 可以帮助刚接触项目不久的开发人员理解代码的运行流程
4.2 日志系统技术实现

日志系统的技术实现主要包括两种类型:

利用printf、std::cout等输出函数将日志信息打印到控制台,但是对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者说数据库方便查询和分析日志,主要分为同步日志和异步日志

4.2.1 同步写日志

同步写日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑,日志输出语句与程序的业务逻辑语句将在同一个线程种运行。每调用一次打印日志API就对应一次系统调用write写日志文件

在高并发场景下,随着日志的数量越来越多,同步日志系统容易产生瓶颈:

  • 一方面,产生日志的速度大于文件写入操作(I/O操作通常比较慢),大量的日志同时等待写入,就会造成线程阻塞,降低系统的整体性能(产生日志与I/O操作速度之间的矛盾)
  • 另一方面,大量的日志打印线程需要同时等待进行write系统调用来写入文件,会产生资源竞争,多个线程需要同时进行写入文件操作(文件写入操作不是原子性的),就需要对写入操作进行同步,同步过程中会涉及到加锁和释放锁,具有一定的性能开销
4.2.2 异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放在放到一个内存缓冲区,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志输出线程完成(作为日志的消费者)

这样的好处是即使日志没有真正的完成输出也不会影响业务线程,以提高程序的性能

  • 一方面,业务线程产生日志后只需要将日志简单的放入一个缓冲区中,这个过程是非常迅速的(因为它只是非常简单的在内存中进行数据存储操作),几乎不涉及复杂的I/O或资源竞争
  • 另一方面,相比文件写入的并发控制,缓冲区写入的并发控制要简单且容易得多,对业务程序性能的消耗也要小得多

5、相关技术知识

5.1 不定参函数

在学C语言的时候,我们就已经接触不定参函数了,例如printf就是一个典型的可以根据格式化字符串解析,对传上来的数据进行格式化的函数

这种不定参函数在实际的使用中非常多见,这里简单的做一下介绍

5.1.1 不定参宏函数

__FILE____LINE__ 是C语言的宏函数,可以用于获取文件名,和代码当前行数,我们可以使用printf打印一条包含当前文件信息和行数信息的日志

#include 
int main() {
    printf("[%s : %d] %s - %d\n", __FILE__, __LINE__, "zdp", 666);   //输出: [test.cpp : 5] zdp - 666
    return 0;
}

但是我们每次打印日志都要写printf,__FILE__,、__LINE__实在是太麻烦了,我们可以使用不定参的宏函数对其进行替换

#define LOG(fmt, ...) printf("[%s : %d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);

解释一下:

  • fmt(format):就是我们的格式化字符串,编译器就是以它为依据进行不定参解析的
  • ...: 就是我们的不定参数
  • "[%s:%d]" fmt 因为这两个都是格式化字符串,C语言是支持直接连接的
  • __VA_ARGS__也是C语言给我们提供的宏函数,用于给我们的fmt传参
  • ##__VA_ARGS__ 加了##是为了告诉编译器,若我们只想传一个不定参数,可以省略前面的fmt参数的传递

##__VA_ARGS__的意思就是
本来我们只想传递一个不定参数需要这么写 LOG(“%s”, “zdp”); 现在可以省略fmt参数的传递 LOG(“zdp”);就可以了

5.1.2 C风格不定参使用

#include 
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

用一段代码来理解这一系列接口的使用

#include 
#include 

#define LOG(fmt, ...) printf("[%s : %d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);

void printNum(int count, ...) {									// count 不定参数的个数
    va_list ap;                                 // va_list实际就是一个char*类型的指针
    va_start(ap, count);                        // 将char*类型指针指向不定参数的起始位置
    for (int i = 0; i < count; i++) {            
        int num = va_arg(ap, int);              // 从ap位置取一个整形大小空间数据拷贝给num,并将ap向后移动一个整形大小空间
        printf("param[%d], %d\n", i, num);       
    }
    va_end(ap);                                 // 将ap指针置空
}

int main() {
    printNum(2, 666, 222);
    return 0;
}
  • va_list ap: 就是定义一个char* 类型的指针
  • va_start : 让指针指向不定参数的起始位置,第二个参数传的是不定参数的前一个参数,因为函数调用过程中是会将实参一个个压入函数栈帧中的,所以参数之间都是紧挨着的。我们找到了前一个参数count的地址,也就等于找到了不定参数的起始地址
  • va_arg : 用于从不定参数中解析参数,第一个参数数据的起始位置,第二个参数指定参数类型,根据类型我们可以推导出参数的大小,从而将参数数据解析出来
  • va_end : 将ap指针置空

这里我们解释传入类型只能是int类型,我们如何使用上述接口将不定参数分离的原理,那么printf这类函数是如何将不定参数分离的呢?这是因为我们在使用printf函数开始传递了format参数,其中包含了%s, %d这类的信息,printf内部通过对format 参数进行解析就知道了后面的参数依次都是什么类型的,然后将类型依次放入va_arg函数,就可以将参数全部提取出来了

void myprintf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    char* res;
    int ret = vasprintf(&res, fmt, ap);
    if (ret != -1) {
        printf(res);
        free(res);
    }
    va_end(ap);
}
  • vasprintf 函数会帮助提取不定参数并且将其拼接到格式化字符串中,并开辟空间将处理好的字符串数据放入空间,并将我们传入的指针指向这块空间
  • 成功返回打印的字节数,失败返回-1

5.1.3 C++风格不定参数使用

void xprintf() {
    std::cout << std::endl;
}
/*C++风格的不定参数*/
template
void xprintf(const T &v, Args&&... args) {
    std::cout << v;
    if ((sizeof...(args)) > 0) {
        xprintf(std::forward(args)...);
    } else {
        xprintf();
    }
}

int main() {
    printNum(2, 666, 222);
    myprintf("%s - %d\n", "clx", 666);
    xprintf("hello");
    xprintf("hello", "world");
    xprintf("hello", "I", " am" , "clx");
    return 0;
}
5.2 设计模式

项目中用到了很多种设计模式,设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它是一套提高代码复用性,可维护性,可读性,稳健性以及安全性的解决方案

5.2.1 六大原则
  1. 单一责任原则(Single Responsibility Principle)
  • 类的职责应该单一,一个方法只做一件事,职责划分清晰,每次改动到最小单位的方法或类
  • 使用建议:两个完全不一样的功能不应该放在一个类中,一个类中应该是一组相关性很高的函数、数据的封装
  • 用例:网络聊天类(❌)应该分割成网络通信类 + 聊天类
  1. 开闭原则(Open Closed Principle)
  • 对扩展开放,对修改封闭(只添加新功能,不修改原有内容)
  • 使用建议:对软件实体的改动,最好用扩展而非修改的方式
  • 用例:超时卖货:商品价格—不是修改商品原来的价格,而是新增促销的价格
  1. 里氏替换原则(Liskov Substitution Principle)
  • 凡事父类能够出现的地方,子类就可以出现,而且替换为子类也不会出现任何的错误或者异常
  • 在继承类时,务必重写父类中的所有方法,尤其注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用
  • 使用建议:子类无比完全实现父类的方法,还子类可以有自己的个性,覆盖或者实现父类的方法时,输入的参数可以被放大,输出也可以缩小
  • 用例:跑步运动员类:会跑步, 子类长跑运动员-会跑步且擅长长跑,子类短跑运动员:会跑步且擅长短跑
  1. 依赖倒置原则(Dependence Inversion Principle)
  • 高层模块不应该依赖底层模块,两者都应该依赖其抽象,不可分隔的原子逻辑就是低层的模式,原子逻辑组装成的就是高层模块
  • 模块间依赖通过抽象(接口)发生,具体类之间不能直接依赖
  • 使用建议:每一个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用
  • 用例:奔驰车司机 – 只能开奔驰,司机类:给什么车开什么车 : 开车的人 : 司机 – 依赖抽象
  1. 迪米特法则(Law of Demeter) 最少知道法则
  • 尽量减少对象之间的交互,从而减少类之间的耦合。一个对象应该对其他对象有最少的了解,对类的低耦合提出了明确的要求:
    只喝直接的朋友交流,朋友间也是有剧烈的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也不对本类造成负面影响,那就放置在本类中)
  • 用例:老师让班长点名,老师给班长名单,班长点名勾选,返回结果。老师只和班长交互,同学们只和班长交互
  1. 接口隔离原则
  • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
  • 使用建议:接口设计尽量精简单一,但是不要对外暴露没有啥意义的接口
  • 用例:修改密码,不应该提供用户信息接口,而是单一使用修改密码接口

从整体上理解六大设计原则,可以简要概括为一句话,用抽象构建框架,用实现扩展细节,具体到每一条设计原则,则对应一条注意事项

5.2.2 单例模式
/* 饿汉单例模式 以空间换时间 */
class Singleton{
public:
    static Singleton& getInstance() { return _eton; }
    int getData() { return _data; }
private:
    Singleton(int data = 99) : _data(data){}
    ~Singleton(){};
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
private:
    static Singleton _eton;
    int _data;
};
Singleton Singleton::_eton;

int main() {
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}
/* 懒汉单例模式 懒加载 -- 延时加载思想 -- 一个对象用的时候再实例化 */
// 这里介绍 作者提出的一种更加优雅简便的单例模式 Meyers Singleton int C++
// C++11后是线程安全的

class Singleton{
public:
    static Singleton& getInstance() {
        static Singleton _eton;
        return _eton;
    }
    int getData() { return _data; }
private:
    Singleton(int data = 99) : _data(data){}
    ~Singleton() {};
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    int _data;
};

int main() {
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}

5.2.3 工厂模式

工厂模式是一种创建型的设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,因此实现创建-使用的分离

工厂模式分为:

  • 简单工厂模式:简单工厂模式实现需要由一个工厂对象通过类型决定创建出来的制定产品类的实例。假设有个工厂可以生产水果,当客户需要产品时明确告知工厂生产哪种水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部取添加新产品的生产方式
class Fruit{
public:
    virtual void name() = 0;
private:   
};

class Apple : public Fruit{
public:
    void name() override{
        std::cout << "I'm a apple" << std::endl;
    }
};

class Banana : public Fruit{
public:
    void name() override {
        std::cout << "I'm a banana" << std::endl;
    }
};

class FruitFactory {
public:
    static std::shared_ptr create(const std::string &name) {
        if (name == "苹果") {
            return std::make_shared();
        } else {
            return std::make_shared();
        }
    }
};

int main() {
    std::shared_ptr fruit = FruitFactory::create("苹果");
    fruit->name();
    fruit = FruitFactory::create("香蕉");
    fruit->name();
    return 0;
}

这个模式的结构和管理产品对象的方式非常简单,但是它的扩展性非常差,当我们需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创造逻辑,违背了开闭原则

  • 工厂方法模式:在简单的工厂模式下新增了多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B两种产品,则开两个工厂,工厂A主要负责生产产品A,工厂B主要生产产品B,用户只要知道产品的工厂名,而不需要知道具体的产品信息,工厂不需要接收客户的产品类别,只负责生产产品
/* 工厂方法模式 */
class Fruit{
public:
    virtual void name() = 0;
private:   
};

class Apple : public Fruit{
public:
    void name() override{
        std::cout << "I'm a apple" << std::endl;
    }
};

class Banana : public Fruit{
public:
    void name() override {
        std::cout << "I'm a banana" << std::endl;
    }
};
class FruitFactory {
public:
    virtual std::shared_ptr createFruit() = 0;
};

class AppleFactory : public FruitFactory {
public:
    virtual std::shared_ptr createFruit() override {
        return std::make_shared();
    }
};

class BananaFactory : public FruitFactory {
public:
    virtual std::shared_ptr createFruit() override {
        return std::make_shared();
    }
};

int main() {
    std::shared_ptr ff(new AppleFactory());
    std::shared_ptr fruit1 = ff->createFruit();
    fruit1->name();
    ff.reset(new BananaFactory());
    std::shared_ptr fruit2 = ff->createFruit();
    fruit2->name();
    return 0;
}

工厂方法模式每次增减一个产品时,都需要增加一个具体的产品类和工厂类,这使得系统中类的个数成倍的增加,在一定程度上增加了系统的耦合度

  • 抽象工厂模式:工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必增加系统的开销,此时我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相互关联的产品组成的家族),由于一个工厂统一生产,这就是抽象工厂模式的基本思想
#include 

/* 简单工厂模式 */
class Fruit{
public:
    virtual void name() = 0;
private:   
};

class Apple : public Fruit{
public:
    void name() override{
        std::cout << "I'm a apple" << std::endl;
    }
};

class Banana : public Fruit{
public:
    void name() override {
        std::cout << "I'm a banana" << std::endl;
    }
};

class Animal {
    public:
        virtual void name() = 0;
};

class Lamp : public Animal {
    public:
        virtual void name() override { 
            std::cout << "I'm a Lamp" << std::endl;
        }
};

class Dog : public Animal {
    public:
        virtual void name() override {
            std::cout << "I'm  a dog" << std::endl;
        }
};

class Factory {
    public: 
        virtual std::shared_ptr getFruit(const std::string& name) = 0;
        virtual std::shared_ptr getAnimal(const std::string& name) = 0;
};

class FruitFactory : public Factory {
    public:
    virtual std::shared_ptr getFruit(const std::string& name) override{
        if (name == "苹果") {
            return std::make_shared();
        } else {
            return std::make_shared();
        }
    }
    virtual std::shared_ptr getAnimal(const std::string& name) override{
        return std::shared_ptr();
    }
};

class AnimalFactory : public Factory {
    public:
    virtual std::shared_ptr getFruit(const std::string& name) override {
        return std::shared_ptr();
    }
    virtual std::shared_ptr getAnimal(const std::string& name) override {
        if (name == "山羊") {
            return std::make_shared();
        } else {
            return std::make_shared();
        }
    }
};

class FactoryProducer {
    public: 
        static std::shared_ptr create(const std::string &name) {
            if (name == "水果") {
                return std::make_shared();
            } else {
                return std::make_shared();
            }
        }
};

int main() {
    std::shared_ptr ff = FactoryProducer::create("水果");
    std::shared_ptr fruit = ff->getFruit("苹果");
    fruit->name();
    fruit = ff->getFruit("香蕉");
    fruit->name();
    ff = FactoryProducer::create("动物");
    std::shared_ptr animal = ff->getAnimal("山羊");
    animal->name();
    animal = ff->getAnimal("小狗");
    animal->name();
    return 0;
}

抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大修改,甚至需要修改抽象层代码,违背了开闭原则

5.2.4 建造者模式

建造者模式是一种创建型的设计模式,使用多个简单对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题

  • 建造者模式主要基于四个核心实现:
  • 抽象产品类
  • 具体产品类:一个具体的产品对象类
  • 抽象Builder类:创建一个产品对象所需要的各个零部件的抽象接口
  • 具体产品的Builder类:实现抽象接口,构建各个部件
  • 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来获取产品
#include 
#include 
#include 

/* 通过MacBook的构造理解建造者模式*/

class Computer{
    public:
        Computer(){};
        void setBoard(const std::string &board) { _board = board; }
        void setDisplay(const std::string &display) { _display = display; }
        virtual void setOs() = 0;
        void showParamaters() {
            std::string param = "Computer Paramaters: \n";
            param += "\tBoard: " + _board + "\n";
            param += "\tDispaly: " + _display + "\n";
            param += "\tOs: " + _os + "\n";
            std::cout << param << std::endl;
        }
    protected:
        std::string _board;
        std::string _display;
        std::string _os;
};
class MacBook : public Computer{
    public:
        virtual void setOs() override {
            _os = "Mac OS x12

你可能感兴趣的:(C++,项目,设计模式,c++)