文章目录
- 异步日志模块
- Logger类实现
- 线程安全LockQueue类实现
异步日志模块
问题:由于RPC服务器端采用了epoll+多线程 ,并发处理来自客户端的请求,所以有可能造成多线程同时写日志信息。
将日志信息写入一个queue中,然后新建日志线程。但是需要注意的是,由于rpcprovider类中是epoll+多线程,所以进而会创建多个日志线程。这样必须要保证线程安全。
于是需要自定义线程安全的queue。思路:如果queue为空,该线程就不抢锁了。
日志生成的命名格式为:年-月-日-log.txt
;内容为时-分-秒+log信息
。
Logger类实现
enum LogLevel
{
INFO, // 普通信息
ENTER, // 错误信息
}
class Logger
{
public:
// 获取日志的单例
static Logger& GetInstance();
// 设置日志级别
void SetLogLevel(LogLevel level);
// 写日志
void Log(std::string msg);
private:
int m_loglevel; // 记录日志级别
LockQueue<std::string> m_lckQue; // 日志缓冲队列
Logger();
Logger(const Logger&) = delete;
Logger(Logger&&) = delete;
}
同样,单例模式记得去除拷贝和移动构造函数。
获取日志的懒汉单例模式:
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();
}
主要功能是将m_lckQue
的内容全部写入文件中。
注意:
线程在等待期间不会一直进行写入操作,而是会暂停等待,直到有新的日志信息被添加到队列中。这是由于队列操作使用了一些同步机制(条件变量或互斥锁),来确保写入线程在队列为空时等待新的日志信息的到来。
而因为日志为单例模式,多次调用宏定义时,只会有一个Logger对象实例存在。而写日志线程是在第一次调用宏定义时创建并启动的,之后的调用并不会创建新的写日志线程。
定义日志宏,让用户不用去实例化Logger类就能用可变参的形式写日志。
#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) \
其中
void Logger::Log(std::string msg)
{
m_lckQue.Push(msg);
}
宏定义体中的具体操作如下:
- 通过调用 Logger::GetInstance() 来获取 Logger 类的单例实例。
- 设置日志级别为 INFO/ERROR,通过调用 logger.SetLogLevel(INFO/ERROR)。
- 创建一个大小为 1024 字节的字符数组 c,并将其初始化为全零。
- 使用 snprintf 函数将格式化的日志消息和可变数量的参数写入字符数组 c 中,最多写入 1024 字节。
- 调用
logger.Log(c)
将字符数组中消息塞入日志系统中的lockqueue中。 - 整个宏定义通过 do-while 语句实现了一个代码块,目的是确保宏定义可以像普通语句一样使用,而不会受限于语法结构。while(0) 是为了确保该宏只能作为单个语句使用,并且在使用时不会引入额外的控制流。
调用方法
LOG_INFO("This is an info message: %s", message);
线程安全LockQueue类实现
- 首先需要封装一个日志队列的自定义的Push和Pop的api接口,通过mutex和conditional_variable来保证线程安全。
- 他的原理类似一个生产者消费者模型,队列Push函数处理的是rpc服务器端的多个worker线程向队列里写数据,写之前加上一把互斥锁,然后push数据,结束以后notify阻塞等待写日志线程向磁盘写数据。
- Pop接口 首先会检测队列是否为空,为空代表没有数据 就会进入阻塞wait状态 然后释放锁 ,有数据来了返回数据。
带锁的LockQueue类是一个模板类,他的定义不能写到lockqueue.cc中。
// 多个worker线程都会写日志queue (宏定义)
void Push(const T &data)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.push(data);
m_condvariable.notify_one();
}
rpc框架调用宏定义,将日志信息Push进入lockqueue。
Logger类写线程负责Pop:
// 一个线程读日志queue,写日志文件
T Pop()
{
std::unique_lock<std::mutex> lock(m_mutex);
while (m_queue.empty())
{
// 日志队列为空,线程进入wait状态
m_condvariable.wait(lock);
}
T data = m_queue.front();
m_queue.pop();
return data;
}
条件变量的意义:
条件变量通常与互斥锁配合使用,用于实现线程之间的等待和唤醒机制。当多个线程需要等待某个条件满足时,它们会调用条件变量的wait()方法进入等待状态,同时释放互斥锁。当条件满足时,另一个线程会调用条件变量的notify_one()或notify_all()方法来唤醒等待的线程。