在一个项目的日志系统里面,我们常常会发现日志模块的实现是使用单例模式。单例模式的特点和它的名字一样,就是一个类能且只能实例化出一个唯一的对象。那么这样做有什么好处呢?
比如,对于日志模块,我们可以先思考它的功能是什么。作为日志,最主要的功能是把系统在运行过程中产生的一些debug、info、warning、error、critical
信息给刷盘到磁盘中。既然是刷盘,那么我们一般会创建出一个子线程来完成刷盘的功能。当提及子线程,我们就需要注意线程安全的问题了。如果有多个日志对象,都可以进行刷盘,但是线程都是抢占式的,可能一个线程记录一半信息,就由于CPU的调度切换到另一个负责刷盘的子线程去了。
结果就是,日志的内容都变成一段一段的不连续信息,刷盘刷得乱七八糟。有同学可能会问:那我加互斥锁!加锁确实可以,但是互斥锁得争夺和之后得阻塞需要陷入内核态,十分消耗时间。此外,就算加了互斥锁,可以保证信息是连续的,但是无法保证信息的时序是一致的。可能一个线程存了10点到12点的日志信息(夸张一点),另一个存了9点到11点的信息,那么在线程刷盘的过程中,时序是相互交叉的。这样也是一种混乱。
因此,从这个角度出发,日志系统通常被设计成单例模式,只有一个对象,按顺序拿到需要记录的日志,再按顺序刷到磁盘中。
单例模式的实现有饿汉式和懒汉式。对于饿汉式,是在用户还没有使用这个对象之前,这个对象就已经存在了。而懒汉式是等到用户调用,才急急忙忙的产生。
下面实现了两种不同的单例模式。
// 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,编译器就会报错了:
补充:双重检测的懒汉式:
#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;
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对象由编译器保证不会初始化两次,第二次初始化不做操作。因此,这种写法是线程安全的。
我们首先设计了一个线程安全的队列。在入队的时候,需要用互斥锁来进行加锁。这把互斥锁使用了C++11中的std::lock_guard
。主要是用来互斥其他线程(写日志线程拿数据Pop、入队Push),而在离开作用域时可以自动释放这把锁。在写入数据后,就应该通知写日志的线程来拿数据,因此,在入队完成之后,我们还会调用notify_one
函数把写日志的线程唤醒。而对于写日志的线程来说,需要用到C++11中的 std::unique_lock
函数。 不用lock_guard
的原因是unique_lock
函数提供了锁的lock
和unlock
操作,而lock_guard
没有。如果队列为空的话,我们就使用m_condvariable.wait(lock)
把这个写日志的线程阻塞休眠,等到入队线程消息的到来。
我们把日志系统设置成为了单例的模式,因为写日志只需要一个专门的对象完成就可以了。在日志系统的构造函数中,首先会开启一个线程,这个线程会不断的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);
源文件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);
}
我们还实现了一个线程安全的队列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;
};