(1)由于RPC服务器端采用了epoll+多线程技术 , 并发处理来自客户端的请求,所以有可能造成多线程同时写日志信息
(2)因此设计了一个线程安全的消息队列(主要采用了互斥锁和条件变量),写日志的时候会先将日志信息放到消息队列中去,再有专门的写日志进程读取消息队列中的日志,写入文件中。
(3)最后,设计了日志模块设计成为了单例模式。
异步缓冲日志队列
单例模式就是一个类只允许创建出一个实例对象。
单例模式的好处主要有两个:
(1) 可以用来解决资源冲突,比如日志模块, 假设两个对象同时写入日志文件或者对共享变量执行修改,就会出现相互覆盖的情况,而单例模式只会产生一个对象,这个对象统一访问共享资源或者竞争资源,可以避免相互覆盖的情况
(2)第二表示全局唯一类,有些数据在系统中只应该保留一份,所以应该设计为单例模式,比如配置文件、全局ID生成器
一开始就加载了实例
#include
using namespace std;
//饿汉式单例模式
class Singleton {
public:
static Singleton* Getinstance() {
static Singleton instance;
return &instance;
}
private:
Singleton() {
cout << "Singleton mode" << endl;
}
};
int main() {
Singleton* test1 = Singleton::Getinstance();
Singleton* test2 = Singleton::Getinstance();
system("pause");
return 0;
}
这种写法在C++98中不是线程安全的,但是在C++11起是线程安全的,因为静态的局部变量在调用的时候分配到静态存储区,所以在编译的时候没有分配。
静态局部对象:
在程序执行到该对象的定义处时,创建对象并调用相应的构造函数!
如果在定义对象时没有提供初始指,则会暗中调用默认构造函数,如果没有默认构造函数,则自动初始化为0。
如果在定义对象时提供了初始值,则会暗中调用类型匹配的带参的构造函数(包括拷贝构造函数),如果没有定义这样的构造函数,编译器可能报错!
直到main()结束后才会调用析构函数!
#include
using namespace std;
//懒汉式单例模式
class Singleton {
public:
static Singleton* Getinstance() {
if (instance == nullptr) {
instance = new Singleton;
}
return instance;
}
private:
Singleton() {
cout << "Singleton mode" << endl;
}
static Singleton* instance;
};
Singleton* Singleton::instance = nullptr; // 这一步很重要!!!!
int main() {
Singleton* test1 = Singleton::Getinstance();
Singleton* test2 = Singleton::Getinstance();
system("pause");
return 0;
}
#endif
特点是延迟加载 当函数被首次访问的时候才加载
#include
#include
using namespace std;
//线程安全懒汉式单例模式
class Singleton {
public:
static Singleton* Getinstance() {
if (instance == nullptr) {
lock_guard<mutex> lc(mtx);
instance = new Singleton;
}
return instance;
}
private:
Singleton() {
cout << "Singleton mode" << endl;
}
static Singleton* instance;
static mutex mtx;
};
mutex Singleton::mtx;
Singleton* Singleton::instance;
int main() {
Singleton* test1 = Singleton::Getinstance();
Singleton* test2 = Singleton::Getinstance();
system("pause");
return 0;
}
(1)线程安全的队列 借助mutex和cv实现了queue
(2)日志模块本身设计为(饿汉式)单例模式
(3)最后,主要实现了两部分功能 一个是rpc服务端worker线程向队列中写入数据 主要利用push接口 log函数实现 然后是 加入写日志线程 向日志文件中写队列的数据 主要利用pop接口(设置分离线程)
(1)首先需要封装一个日志队列的自定义的push和pop的api接口,通过mutex和conditional_variable来保证线程安全。
(2)他的原理类似一个生产者消费者模型,队列push函数处理的是rpc服务器端的多个worker线程向队列里写数据,写之前加上一把互斥锁,然后push数据,结束以后notify阻塞等待写日志线程向磁盘写数据。
(3)pop接口 首先会检测队列是否为空 为空代表没有数据 就会进入阻塞wait状态 然后释放锁 ,有数据来了返回数据。
#pragma once
#include
#include
#include // pthread_mutex_t 线程互斥
#include // pthread_condition_t 线程通信
// 模板代码 不能写在cc文件中
// 异步写日志的日志队列
template
class LockQueue {
public:
//muduo 提供的 多个worker线程都会写日志queue
void Push(const T &data) {
std::lock_guard lock(m_mutex); // 获得互斥锁
m_queue.push(data);
m_condvariable.notify_one(); // 唤醒wait线程
}
// 出右括号释放锁
// 一个线程 在读日志queue,写日志文件
T Pop() {
std::unique_lock lock(m_mutex);
while (m_queue.empty()) {
// 日志队列为空, 线程进入wait状态
m_condvariable.wait(lock); // 进入wait等待状态 释放锁
}
T data = m_queue.front();
m_queue.pop();
return data;
}
private:
std::queue m_queue;
std::mutex m_mutex;
std::condition_variable m_condvariable;
};
#pragma once
#include "lockqueue.h"
#include
enum LogLevel {
INFO, // 普通信息
ERROR, // 错误信息
};
//mprpc框架提供的日志系统
class Logger {
public:
//获取日志的单例
static Logger& GetInstance();
//设置日志级别
void SetLogLevel(LogLevel level);
//写日志
void Log(std::string msg);
private:
int m_loglevel; // 记录日志级别
LockQueue m_lckQue; // 日志缓冲队列
Logger(); // 设置为单例模式
Logger(const Logger&) = delete; // 防止通过拷贝构造生成新对象
Logger(Logger&&) = delete;
};
//定义宏 可变参 LOG_INFO("xxx %d %s", 20, "xxx")
#define LOG_INFO(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(INFO); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while (0)
#define LOG_ERR(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(ERROR); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while (0)
#include "logger.h"
#include "time.h"
#include
Logger& Logger::GetInstance() {
static Logger logger;
return logger;
}
Logger::Logger() {
// 启动专门的写日志进程
std::thread writeLogTask([&]() {
for (;;) {
// 获取当天的日期, 然后取日志信息, 写入相应的日志文件当中 a+
time_t now = time(nullptr);
tm *nowtm = localtime(&now);
char file_name[128];
sprintf(file_name, "%d-%d-%d-log.txt", nowtm->tm_year + 1900, nowtm->tm_mon + 1, nowtm->tm_mday);
FILE *pf = fopen(file_name, "a+");
if (pf == nullptr) {
std::cout << "logger file : "<< file_name << "open error!" << std::endl;
exit(EXIT_FAILURE);
}
std::string msg = m_lckQue.Pop(); // 从异步日志队列中读数据
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"));
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(std::string msg) {
//
m_lckQue.Push(msg); // 从worker线程中把数据写入queue
}
//定义宏 可变参 LOG_INFO("xxx %d %s", 20, "xxx")
#define LOG_INFO(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(INFO); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while (0)
自定义打印时,用到可变参数,用...
即可表示可变参数,如下:
#include
#define LOG1(...) printf(__VA_ARGS__)//...表示可变参数,__VA_ARGS__就是将...的值复制到这里
int main(int argc, char** argv)
{
char *str = "test __VA_ARGS__";
int num = 10086;
LOG1("this is test __VA_ARGS__\r\n");
LOG1("this is test __VA_ARGS__:%s, %d\r\n", str, num);
return 0;
}
打印结果如下:
this is test __VA_ARGS__
this is test __VA_ARGS__:test __VA_ARGS__, 10086
VA_ARGS__就是将…的值复制到这里
int main(int argc, char** argv)
{
char *str = “test VA_ARGS”;
int num = 10086;
LOG1(“this is test VA_ARGS\r\n”);
LOG1(“this is test VA_ARGS:%s, %d\r\n”, str, num);
return 0;
}
打印结果如下:
```cpp
this is test __VA_ARGS__
this is test __VA_ARGS__:test __VA_ARGS__, 10086