文章目录
同步/异步日志系统
同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备.
- 自定义阻塞队列
- 单例模式创建日志
- 同步日志
- 异步日志
- 实现按天、超行分类
共两个个class,分别是阻塞队列block_queue和日志log
一、阻塞队列 block_queue
循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;
为了线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
1.头文件
#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
using namespace std;
2.类的定义与方法实现
阻塞队列类直接定义在.h头文件中并对方法直接进行实现
template <class T>
class block_queue
{
public:
// 构造函数
block_queue(int max_size = 1000)
{
if (max_size <= 0)
{
exit(-1);
}
m_max_size = max_size;
// 在堆上动态分配了一个大小为max_size的数组m_array来存储队列元素
m_array = new T[max_size];
m_size = 0;
// 由于刚创建时队列为空,所以m_front和m_back初始化为-1
m_front = -1;
m_back = -1;
}
// 清空队列中的所有元素
void clear()
{
// 由于这是一个多线程环境下的代码,所以需要使用互斥锁来保证多个线程之间对队列的操作是同步的,以避免出现竞态条件
m_mutex.lock();
m_size = 0;
m_front = -1;
m_back = -1;
m_mutex.unlock();
}
// 析构函数
~block_queue()
{
m_mutex.lock();
// 检查m_array指针是否为NULL,如果不为NULL,则使用delete[]运算符删除m_array所指向的动态分配的数组,释放内存空间
if (m_array != NULL)
delete [] m_array;
m_mutex.unlock();
}
//判断队列是否满了
bool full()
{
m_mutex.lock();
if (m_size >= m_max_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//判断队列是否为空
bool empty()
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素
bool front(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回队尾元素
bool back(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}
// 返回队列中元素的个数
int size()
{
int tmp = 0;
m_mutex.lock();
tmp = m_size;
m_mutex.unlock();
return tmp;
}
// 返回队列的最大容量
int max_size()
{
int tmp = 0;
m_mutex.lock();
tmp = m_max_size;
m_mutex.unlock();
return tmp;
}
//往队列添加元素,需要将所有使用队列的线程先唤醒
//当有元素push进队列,相当于生产者生产了一个元素
//若当前没有线程等待条件变量,则唤醒无意义
bool push(const T &item)
{
m_mutex.lock();
if (m_size >= m_max_size)
{
// 队列已满,调用条件变量m_cond的broadcast()方法,通知所有等待在该条件变量上的线程
m_cond.broadcast();
m_mutex.unlock();
return false;
}
// 将新元素存储在队尾位置
m_back = (m_back + 1) % m_max_size;
m_array[m_back] = item;
// 将队列中元素的个数m_size加1
m_size++;
// 调用条件变量m_cond的broadcast()方法,通知所有等待在该条件变量上的线程,即通知有可能在pop操作中等待的线程可以执行了
m_cond.broadcast();
m_mutex.unlock();
return true;
}
//pop时,如果当前队列没有元素,将会等待条件变量
bool pop(T &item)
{
m_mutex.lock();
while (m_size <= 0)
{
// 如果队列为空,则调用条件变量m_cond的wait()方法,等待其他线程向队列中插入元素
if (!m_cond.wait(m_mutex.get()))
{
m_mutex.unlock();
return false;
}
}
// 当取出了队列中的一个元素时,该函数将队头下标m_front加1 m_front指向实际元素的前一个位置
m_front = (m_front + 1) % m_max_size;
// 并将队头元素m_array[m_front]赋值给输出参数item
item = m_array[m_front];
// 将队列中元素的个数m_size减1,表示队列中的元素数量减少了一个
m_size--;
m_mutex.unlock();
return true;
}
//增加了超时处理
bool pop(T &item, int ms_timeout)
{
struct timespec t = {0, 0};
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
m_mutex.lock();
if (m_size <= 0)
{
t.tv_sec = now.tv_sec + ms_timeout / 1000;
t.tv_nsec = (ms_timeout % 1000) * 1000;
if (!m_cond.timewait(m_mutex.get(), t))
{
m_mutex.unlock();
return false;
}
}
if (m_size <= 0)
{
m_mutex.unlock();
return false;
}
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}
private:
locker m_mutex;
cond m_cond;
T *m_array;
int m_size;
int m_max_size;
int m_front;
int m_back;
};
二、日志
1.头文件
#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"
using namespace std;
2.类的定义
class Log
{
public:
//C++11以后,使用局部变量懒汉不用加锁
static Log *get_instance()
{
static Log instance;
return &instance;
}
// 是一个线程函数,用于异步写日志
static void *flush_log_thread(void *args)
{
// 通过调用静态成员函数Log::get_instance()获取Log类的单例实例,并通过该实例调用 async_write_log()方法进行日志输出操作
Log::get_instance()->async_write_log();
}
//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);
void write_log(int level, const char *format, ...);
void flush(void);
private:
Log();
virtual ~Log();
// 从阻塞队列中取出一个日志字符串,然后将该字符串写入文件
void *async_write_log()
{
// 使用string类型的变量single_log来存储要写入文件的单个日志记录
string single_log;
//从阻塞队列中取出一个日志string,写入文件
while (m_log_queue->pop(single_log))
{
m_mutex.lock();
fputs(single_log.c_str(), m_fp);
m_mutex.unlock();
}
}
private:
char dir_name[128]; //路径名
char log_name[128]; //log文件名
int m_split_lines; //日志最大行数
int m_log_buf_size; //日志缓冲区大小
long long m_count; //日志行数记录
int m_today; //因为按天分类,记录当前时间是那一天
FILE *m_fp; //打开log的文件指针
char *m_buf;
block_queue<string> *m_log_queue; //阻塞队列
bool m_is_async; //是否同步标志位
locker m_mutex;
int m_close_log; //关闭日志
};
3.宏定义
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}
4.构造函数与析构函数
构造函数, m_count 成员变量初始化为0,并将 m_is_async 成员变量初始化为false
Log::Log()
{
m_count = 0;//日志行数记录
m_is_async = false;//是否同步标志位
}
析构函数,用于释放资源
Log::~Log()
{
if (m_fp != NULL)
{
// 调用fclose函数来关闭日志文件,并释放文件指针相关的资源
fclose(m_fp);
}
}
5.init()方法
日志类的初始化函数,用于对日志对象进行初始化。该函数接受一些参数,包括日志文件名、是否关闭日志、缓冲区大小、每个日志文件最大行数和最大队列大小等。函数返回布尔类型的值,表示初始化是否成功。
//异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size)
{
//如果设置了max_queue_size,则设置为异步
if (max_queue_size >= 1)
{
m_is_async = true;
// 创建一个阻塞队列对象m_log_queue
m_log_queue = new block_queue<string>(max_queue_size);
pthread_t tid;
//flush_log_thread为回调函数,这里表示创建线程异步写日志
pthread_create(&tid, NULL, flush_log_thread, NULL);
}
// 将传入参数赋值给成员变量
m_close_log = close_log;
m_log_buf_size = log_buf_size;
m_buf = new char[m_log_buf_size];
memset(m_buf, '\0', m_log_buf_size);
m_split_lines = split_lines;
// 获取当前系统时间,以便生成日志文件名
time_t t = time(NULL);
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;
// 根据传入的日志文件路径和名称,使用strrchr()函数找到最后一个斜杠字符'/'的位置
const char *p = strrchr(file_name, '/');
char log_full_name[256] = {0};
// 如果不存在,则说明只有文件名,没有路径,直接将日期和文件名组合起来作为新的日志文件名
if (p == NULL)
{
snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
}
// 否则,分别取出路径和文件名,将它们与日期组合作为日志文件名
else
{
strcpy(log_name, p + 1);
strncpy(dir_name, file_name, p - file_name + 1);
snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
}
// 将m_today成员变量设置为当前日期
m_today = my_tm.tm_mday;
// 使用fopen()函数打开日志文件,以追加方式写入日志
m_fp = fopen(log_full_name, "a");
// 如果打开文件失败,则返回false
if (m_fp == NULL)
{
return false;
}
return true;
}
6.write_log()方法
// level代表日志级别(0-DEBUG,1-INFO,2-WARNING,3-ERROR),format代表格式化字符串
void Log::write_log(int level, const char *format, ...)
{
struct timeval now = {0, 0};
gettimeofday(&now, NULL);// 使用 gettimeofday() 获取当前时间
time_t t = now.tv_sec;
struct tm *sys_tm = localtime(&t);// 使用 localtime() 将当前时间转换成本地时间
struct tm my_tm = *sys_tm;
char s[16] = {0};
// 根据日志级别添加不同的标记
switch (level)
{
case 0:
strcpy(s, "[debug]:");
break;
case 1:
strcpy(s, "[info]:");
break;
case 2:
strcpy(s, "[warn]:");
break;
case 3:
strcpy(s, "[erro]:");
break;
default:
strcpy(s, "[info]:");
break;
}
//写入一个log,对m_count++, m_split_lines最大行数
m_mutex.lock();
m_count++;
// 对日志文件进行写入操作,实现了按照日期和行数分割日志文件的功能。如果发现当前日期与之前记录的日期不同,或者已经写入的行数达到某个阈值 m_split_lines,则创建新的日志文件
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log
{
char new_log[256] = {0};
fflush(m_fp);
fclose(m_fp);
char tail[16] = {0};
snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
if (m_today != my_tm.tm_mday)
{
snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
m_today = my_tm.tm_mday;
m_count = 0;
}
else
{
snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
}
m_fp = fopen(new_log, "a");
}
m_mutex.unlock();
// 使用可变参数列表的方式获取传入的日志格式化字符串及其参数
va_list valst;
va_start(valst, format);
string log_str;
m_mutex.lock();
//写入的具体时间内容格式
int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
// 使用vsnprintf()将可变长参数列表中的数据按照format格式化,并将格式化后的字符串添加到日志缓冲区中
int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);
m_buf[n + m] = '\n';
m_buf[n + m + 1] = '\0';
log_str = m_buf;
m_mutex.unlock();
// 如果开启了异步写入模式,将日志缓冲区中的内容加入到日志队列中
if (m_is_async && !m_log_queue->full())
{
m_log_queue->push(log_str);
}
// 否则直接将日志信息写入到日志文件中
else
{
m_mutex.lock();
fputs(log_str.c_str(), m_fp);
m_mutex.unlock();
}
// 返回之前需要使用va_end()关闭可变长参数列表
va_end(valst);
}
7. flush()方法
强制刷新写入流缓冲区,确保之前的输出内容被写入到文件中
void Log::flush(void)
{
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}