• 设计模式:饿汉式和懒汉式单例模式(C++实现)以及日志系统的实现


    1. 背景

    在一个项目的日志系统里面,我们常常会发现日志模块的实现是使用单例模式。单例模式的特点和它的名字一样,就是一个类能且只能实例化出一个唯一的对象。那么这样做有什么好处呢?

    比如,对于日志模块,我们可以先思考它的功能是什么。作为日志,最主要的功能是把系统在运行过程中产生的一些debug、info、warning、error、critical信息给刷盘到磁盘中。既然是刷盘,那么我们一般会创建出一个子线程来完成刷盘的功能。当提及子线程,我们就需要注意线程安全的问题了。如果有多个日志对象,都可以进行刷盘,但是线程都是抢占式的,可能一个线程记录一半信息,就由于CPU的调度切换到另一个负责刷盘的子线程去了。

    结果就是,日志的内容都变成一段一段的不连续信息,刷盘刷得乱七八糟。有同学可能会问:那我加互斥锁!加锁确实可以,但是互斥锁得争夺和之后得阻塞需要陷入内核态,十分消耗时间。此外,就算加了互斥锁,可以保证信息是连续的,但是无法保证信息的时序是一致的。可能一个线程存了10点到12点的日志信息(夸张一点),另一个存了9点到11点的信息,那么在线程刷盘的过程中,时序是相互交叉的。这样也是一种混乱。

    因此,从这个角度出发,日志系统通常被设计成单例模式,只有一个对象,按顺序拿到需要记录的日志,再按顺序刷到磁盘中。

    2. 单例模式实现

    单例模式的实现有饿汉式和懒汉式。对于饿汉式,是在用户还没有使用这个对象之前,这个对象就已经存在了。而懒汉式是等到用户调用,才急急忙忙的产生。

    下面实现了两种不同的单例模式。

    2.1 饿汉式的单例模式

    // 1. 饿汉式的单例
    class Singleton {
    public:
    	Singleton* getInstance() { return &instance; }
    private:
    	static Singleton instance;
    	Singleton() {}
    	Singleton(const Singleton&) = delete;
    	Singleton& operator=(const Singleton&) = delete;
    };
    Singleton Singleton::instance;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于上面这个饿汉式的单例模式,有几点需要注意的地方:

    • 为什么得到这个对象的外部接口函数getInstance需要是static的?

    • 答:因为对于单例模式,你得到对象的唯一方式就是这个接口。那在得到对象之前,你没有对象,怎么调用一个类的函数呢?我们知道,static函数属于一个类,而不属于任何特定的对象,因此,需要使用static类型来实现这个接口,以便在没有对象的时候,调用一个类的函数。

    • 为什么这个唯一的对象需要设计成static类型的?

    • 答:因为前面我们设计一个static的函数来获取对象,而static成员函数只能访问static类型的成员。因此,我们不得已只能将这个对象设计成static类型的了。

    如果设计成非static,编译器就会报错了:

    在这里插入图片描述

    • 为什么需要把拷贝构造函数和拷贝赋值函数设计成delete的?
      答:因为我们只需要一个对象。允许使用两个函数来生产出其他的对象。
    • 其余需要注意的是static对象需要类外初始化。类内只是声明。

    补充:双重检测的懒汉式:

    #include 
    #include 
    
    using namespace std;
    
    class Singleton {
    public:
    private:
    	static Singleton* instance;
    	Singleton() {};
    	Singleton(const Singleton&) = delete;
    	Singleton& operator=(const Singleton&) = delete;
    
    public:
    	static Singleton* getInstance() {
    		mutex mtx_;
    		if (instance == nullptr) {
    			unique_lock<mutex> lock(mtx_);
    			if (instance == nullptr) {
    				instance = new Singleton();
    			}
    			lock.unlock();
    		}
    		return instance;
    	}
    };
    Singleton* Singleton::instance = nullptr;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.2 懒汉式的单例模式

    class Singleton {
    public:
    	static Singleton* getInstance() {
    		static Singleton instance;  //在函数里面定义局部对象,运行到这一句才产生对象
    		return &instance;
    	}
    private:
    	Singleton() {};
    	Singleton(const Singleton&) = delete;
    	Singleton& operator=(const Singleton&) = delete;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于懒汉式的单例模式,我们只需要注意把这个static对象放入函数,成为一个静态的局部对象就可以了,这样在运行到这个语句的时候才进行对象的初始化。

    此外,由于饿汉式的单例不是局部的静态变量,因此在main函数执行之前,对象就已经初始化完成了,所以不需要考虑线程安全的问题。而对于懒汉式的单例,是运行到定义的语句才进行初始化,那么,有没一种可能:两个线程同时进入getInstance函数,把我们的对象初始化两次?

    回答:有可能,但static对象由编译器保证不会初始化两次,第二次初始化不做操作。因此,这种写法是线程安全的。

    3. 基于单例模式实现线程安全的日志模块

    在这里插入图片描述

    1. 我们首先设计了一个线程安全的队列。在入队的时候,需要用互斥锁来进行加锁。这把互斥锁使用了C++11中的std::lock_guard lock(mutex)。主要是用来互斥其他线程(写日志线程拿数据Pop、入队Push),而在离开作用域时可以自动释放这把锁。在写入数据后,就应该通知写日志的线程来拿数据,因此,在入队完成之后,我们还会调用notify_one函数把写日志的线程唤醒。而对于写日志的线程来说,需要用到C++11中的 std::unique_lock lock(mutex)函数。 不用lock_guard的原因是unique_lock函数提供了锁的lockunlock操作,而lock_guard没有。如果队列为空的话,我们就使用m_condvariable.wait(lock)把这个写日志的线程阻塞休眠,等到入队线程消息的到来。

    2. 我们把日志系统设置成为了单例的模式,因为写日志只需要一个专门的对象完成就可以了。在日志系统的构造函数中,首先会开启一个线程,这个线程会不断的Pop出消息,把日志信息写到磁盘IO中。

    头文件logger.h:

    #pragma once
    #include
    #include "lockqueue.h"
    #include 
    
    enum LogLevel{
        INFO, // 普通信息
        ERRO  // 错误信息
    };
    
    class Logger {
    public:
        // 获取日志的单例
        static Logger& GetLoggerInstance();
        // 设置日志级别
        void setLogLevel(LogLevel level);
        // 写日志
        void Log(const std::string msg);
    
    private:
        // 日志级别
        int m_loglevel;
        // 日志缓冲队列
        LockQueue<std::string> m_lckQue;
    
        // 设置成单例模式
        Logger();
        Logger(const Logger&) = delete;
        Logger& operator=(const Logger&) = delete;
    };
    
    #define LOG_ERR(logmsgformat, ...) \
        do \
        {  \
            Logger &logger = Logger::GetLoggerInstance(); \
            logger.setLogLevel(ERRO); \
            char c[1024] = {0}; \
            snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
            logger.Log(c); \
        } while(0) \
    
    // 定义宏
    #define LOG_INFO(logmsgformat, ...) \
        do \
        { \
            Logger& logger = Logger::GetLoggerInstance(); \
            logger.setLogLevel(INFO); \
            char c[1024] = {0}; \
            snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
            logger.Log(c); \
        } while (0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    源文件logger.cpp:

    #include "logger.h"
    #include "time.h"
    #include 
    
     // 获取日志的单例
    Logger& Logger::GetLoggerInstance() {
        static Logger logger;
        return logger;
    }
    
    // 启动专门的写日志线程
    Logger::Logger() {
        std::thread writeLogTask(
            [&](){
                for (;;) {
                    // 获取日期 从队列中获取日志信息 追加到文件中
                    time_t now = time(nullptr);
                    tm* nowtm = localtime(&now);
    
                    char filename[128] = {0};
                    sprintf(filename, "%d-%d-%d-log.txt", 
                                        nowtm->tm_year + 1900, nowtm->tm_mon + 1, nowtm->tm_mday);
    
                    FILE* pf = fopen(filename, "a+");
                    if (nullptr == pf) {
                        std::cout << "logger file " << filename <<  " open error" << std::endl; 
                        exit(EXIT_FAILURE);
                    }
                    // 插入时间前缀
                    char time_buf[128] = {0};
                    sprintf(time_buf, "%d:%d:%d => [%s]", 
                                        nowtm->tm_hour, 
                                        nowtm->tm_min, 
                                        nowtm->tm_sec,
                                        (m_loglevel == INFO) ? "info" : "error");
                    std::string msg = m_lckQue.Pop();
                    msg.insert(0, time_buf);
                    msg.append("\n");
                    fputs(msg.c_str(), pf);
                    fclose(pf);
                }
            }
        );
        // 设置线程分离
        writeLogTask.detach();
    }
    // 设置日志级别
    void Logger::setLogLevel(LogLevel level) {
        m_loglevel = level;
    }
    
    // 写日志 将日志放到lockqueue缓冲队列中
    void Logger::Log(const std::string msg) { 
        m_lckQue.Push(msg);
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    我们还实现了一个线程安全的队列lockqueue.h:

    #pragma once
    #include 
    #include 
    #include 
    #include 
    
    // 异步写日志的缓冲队列
    template<typename T>
    class LockQueue {
    public:
        // 多个线程都会把日志写入缓冲队列
        void Push(const T& data) {
            std::lock_guard<std::mutex> lock(m_mutex);
            m_queue.push(data);
            m_condvariable.notify_one();
        }
        // 一个线程负责取出缓冲队列的日志 写入磁盘I/O中
        T Pop() {
            std::unique_lock<std::mutex> lock(m_mutex);
            while (m_queue.empty()) {
                // 日志队列为空 进入等待状态
                m_condvariable.wait(lock);
            }
            T data = m_queue.front();
            m_queue.pop();
            return data;
        }
    private:
        std::queue<T> m_queue;
        std::mutex m_mutex;
        std::condition_variable m_condvariable;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
  • 相关阅读:
    【数据结构】链表OJ第二篇 —— 链表的中间节点 && 链表中倒数第k个节点 && 链表分割 && 链表的回文结构 && 相交链表
    从零点五开始的深度学习笔记——VAE(Variational AutoEncoder) (一) 预备知识
    【老师见打系列】:我只是写了一个自动回复讨论的脚本~
    prompt 综述
    Redis —— Could not connect to Redis at 127.0.0.16379 由于目标计算机积极拒绝,无法连接。
    LeetCode994. 腐烂的橘子(C++中等题)
    【java学习】this关键字(27)
    痞子衡嵌入式:不同J-Link版本对于i.MXRT1170连接复位后处理行为有所不同
    NX二次开发后处理中保存tcl变量值到文本
    无涯教程-JavaScript - BETA.INV函数
  • 原文地址:https://blog.csdn.net/zsiming/article/details/126782954