设计模式:饿汉式和懒汉式单例模式(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;

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

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

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

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

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

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

在这里插入图片描述

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

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

#include <iostream>
#include <mutex>

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;

2.2 懒汉式的单例模式

class Singleton {
public:
	static Singleton* getInstance() {
		static Singleton instance;  //在函数里面定义局部对象,运行到这一句才产生对象
		return &instance;
	}
private:
	Singleton() {};
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

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

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

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

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

在这里插入图片描述

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

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

头文件logger.h:

#pragma once
#include<string>
#include "lockqueue.h"
#include <iostream>

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);

源文件logger.cpp:

#include "logger.h"
#include "time.h"
#include <iostream>

 // 获取日志的单例
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);
}    

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

#pragma once
#include <queue>
#include <mutex>
#include <thread>
#include <condition_variable>

// 异步写日志的缓冲队列
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
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值