log.h
文件定义了一个单例模式的日志类 Log
,用于记录系统日志。
单例设计模式:
主要功能
根据上述分析,这个日志类 Log
主要实现了以下功能:
1. 日志写入
该日志类提供了 write_log()
方法用于将日志内容写入文件。日志内容可以包含不同的 日志级别,如 DEBUG
、INFO
、WARN
、ERROR
,通过传入日志级别来选择日志前缀。
日志内容支持可变参数,类似于 printf()
的格式化方式,通过 va_list
来处理变长参数,方便使用者记录不同类型的信息。
1.1 异步日志写入
当日志量很大时,直接同步写入可能影响程序的响应时间。该日志类支持 异步模式,可以将日志写入信息推入 阻塞队列 中,通过后台线程异步地将日志内容写入文件。
1.2 同步日志写入
该日志类默认支持 同步模式,直接将日志内容写入文件。如果没有设置异步模式,则所有日志操作都在调用 write_log()
时直接写入文件。这种方式适用于日志量较小且对性能要求不高的场景。
在同步写入时使用了 互斥锁(std::lock_guard
)确保多线程环境下对文件写入的安全性,避免多个线程同时写入文件时出现数据混乱的问题。
4. 日志文件的自动分割
为了防止单个日志文件过大,该日志类实现了 日志文件的自动分割功能:
- 当日志文件的行数超过设定的最大行数(
m_split_lines
)时,自动创建一个新的日志文件继续记录,避免文件过大导致的管理和读取困难。 - 日志也会按照日期进行分割,当进入新的一天时,创建一个新的日志文件,以便后续管理和查找。
5. 日志缓冲区管理
在初始化时为日志创建了一个缓冲区(m_buf
),用于格式化日志内容。写入日志前,将日志内容格式化存储到缓冲区中,然后根据同步或异步模式决定写入方式。
使用缓冲区可以提升日志写入性能,因为可以在内存中组织日志内容,再一次性写入文件,减少磁盘 I/O 操作。
6. 日志内容的格式化
日志内容被格式化为标准化的时间信息加上日志级别标签的字符串,方便后续进行日志的查看和分析。
使用 gettimeofday()
函数获取精确的时间戳(包括秒和微秒),保证日志时间的精度。
7. 后台线程管理
在异步模式下,创建了一个后台线程来从阻塞队列中获取日志并写入文件。
使用了 pthread_create()
来创建线程,并且通过 pthread_detach()
将线程设置为 分离状态,确保后台线程在完成后自动释放资源,不需要显式调用 pthread_join()
。
8. 日志队列管理
使用了一个 阻塞队列 (block_queue
) 来保存待写入的日志内容。当异步模式开启时,日志内容被推入队列,后台线程从队列中取出日志进行写入。
如果队列满了,日志会被丢弃,并输出警告信息,提醒开发者日志队列的状态。这样可以有效防止队列无限增长,导致内存使用超标。
9. 日志刷新功能
提供了 flush()
方法,用于 强制刷新输出缓冲区,将缓存中的日志内容立即写入文件,确保在程序异常或终止时日志不会丢失。
flush()
使用互斥锁确保在多线程环境中对日志文件刷新操作的线程安全。
10. 错误处理与资源管理
在初始化过程中,针对文件打开失败、内存分配失败等情况都提供了合理的错误处理逻辑,避免程序在初始化失败后继续执行。
通过析构函数 (~Log()
) 实现了对所有分配资源的清理,包括日志文件指针的关闭、内存的释放等,确保程序在退出时不会发生资源泄漏。
11. 日志丢弃与警告机制
当日志队列满时,当前日志会被丢弃。并且对丢弃的日志进行计数,并每丢弃 100 条日志输出一次警告,提醒开发者系统负载过高。
这能够帮助开发者了解系统的状态,尤其是在高负载情况下可能需要采取措施(例如增大日志队列的容量或优化日志生成逻辑)。
12. 线程安全
为了保证多线程环境下日志写入的安全性,类中的 写入日志和文件操作都被互斥锁保护。使用 std::lock_guard<locker>
来管理互斥锁,确保在多线程环境中不会出现数据竞争的问题。
通过线程安全的 localtime_r()
来替代 localtime()
,保证在多线程环境下时间转换的安全性。
代码解析
这里的日志类采用了单例模式的懒汉式设计模式,即需要使用日志时才实例化唯一的一个日志类,并提供一个全局访问点来访问。
阻塞队列
Log
类中,阻塞队列是使用模板类 block_queue<string>
定义的,意味着它存放的是 string
类型 的数据。
该阻塞队列 m_log_queue
用于在异步写日志时存放待写入的日志消息。当需要记录日志时,日志内容会以字符串的形放入这个阻塞队列中。后台线程会从队列中取出日志消息,然后写入日志文件。
主要功能
- 线程安全的队列操作:
通过互斥锁 (locker m_mutex) 确保对队列的所有操作(如添加、移除、查看队首/队尾等)都是线程安全的。每次进行队列操作之前都需要先获取互斥锁,操作结束后再解锁。 - 阻塞与条件变量:
使用条件变量 (cond m_cond) 实现生产者-消费者模式。
○ 当调用 push() 往队列中添加元素时,如果有消费者在等待队列不为空,则会通过条件变量通知消费者。
○ 当调用 pop() 从队列中取出元素时,如果队列为空,则消费者会等待直到队列中有可用元素。 - 循环数组实现:
○ 使用循环数组实现队列结构,防止频繁的内存分配和释放,最大化地提高了队列的性能。
○ m_back = (m_back + 1) % m_max_size; 实现了循环数组的功能,确保队列头尾位置可以循环利用。 - 队列状态检查:
○ 提供了 full()、empty()、size() 等方法,用于检查队列的当前状态,判断是否已满或为空,以及获取当前的队列大小。 - 超时等待功能:
○ 在 pop() 方法中增加了超时版本,调用者可以设定超时时间(毫秒),在指定时间内等待队列中的元素变为可用。超时后将返回失败状态,避免长时间阻塞。
为什么使用阻塞队列
- 异步日志
通过阻塞队列实现 异步日志写入,可以显著提高日志记录的性能。
主线程将日志内容放入队列后继续执行,后台线程在空闲时从队列中取出日志进行写入。
- 提高响应速度
异步日志系统可以让主线程将日志写入队列而不需要等待文件写入完成,从而降低日志写入对业务逻辑的阻塞影响,尤其是在高并发场景下。
- 线程安全
阻塞队列通常是线程安全的,能够确保多个线程之间安全地访问队列中的数据,避免竞争条件。
改进建议
- 目前使用
m_mutex.lock()
和m_mutex.unlock()
来进行资源的上锁和解锁
改进: 可以考虑使用std::lock_guard
或者std::unique_lock
,以减少手动管理锁的复杂性。
std::lock_guard
是 C++ 标准库中的一种同步原语工具,主要用于管理互斥锁的生命周期以确保线程安全。在多线程编程中,它可以自动对给定的互斥锁进行加锁操作,在
std::lock_guard
对象的生命周期内,保持互斥锁处于锁定状态。当std::lock_guard
对象被销毁时,它会自动对互斥锁进行解锁操作,从而避免了因忘记解锁互斥锁而导致的死锁等问题。使用
std::lock_guard
时,需要引用头文件<mutex>
std::lock_guard
是 RAII(资源获取即初始化)模式的实现,通过在作用域结束时自动释放锁,来保证锁的安全管理。
/* 从阻塞队列中取日志写入文件 */
void *async_write_log()
{string single_log;/* 从阻塞队列中取出一个日志string,写入文件 */while (m_log_queue->pop(single_log)){std::lock_guard<locker> lock(m_mutex); /* 使用 lock_guard 自动管理互斥锁的生命周期 */fputs(single_log.c_str(), m_fp);}
}
- 加入异常处理,以防止在日志写入过程中出现不可预期的错误导致线程崩溃。
/* 线程入口函数,用于异步写入日志 */
static void *flush_log_thread(void *args)
{try{// 获取日志单例并执行异步写入Log::get_instance()->async_write_log();}catch (const std::exception &e){std::cerr << "Exception in flush_log_thread: " << e.what() << std::endl;}return nullptr;
}
localtime()
不是线程安全的,因为它返回的是静态的tm
结构体。如果多个线程同时调用localtime()
,返回的内容可能会发生冲突。
改进: 对于线程安全的版本,可以使用localtime_r()
(POSIX); 检查localtime_r()
的返回值,可以防止时间转换失败时程序出现崩溃问题。
time_t t = time(NULL); /* 获取当前的系统时间,返回的是从 1970 年 1 月 1 日(即 Unix 纪元)到现在的秒数 */
struct tm my_tm; /* 包含了当前的本地时间信息 */
if (localtime_r(&t, &my_tm) == nullptr) {std::cerr << "Failed to get local time." << std::endl;return false;
}
- 异步写入日志时,如果阻塞队列已满,会直接进入同步写入逻辑,在高负载场景下,可能会导致主线程因为写日志而阻塞,产生性能瓶颈。
改进: 在异步写入且阻塞队列已满的情况下,丢弃当前日志,并通过标准错误流输出警告,避免产生性能瓶颈。
/* 异步写入 && 阻塞队列未满 */
if (m_is_async && !m_log_queue->full())
{m_log_queue->push(log_str); /* 将日志字符串 log_str 推入日志队列 */
}
else
{/* 如果队列已满,选择丢弃当前日志,并增加 dropped_logs 计数器。 */static int dropped_logs = 0;if (m_is_async && m_log_queue->full()){dropped_logs++;if (dropped_logs % 100 == 0){/* 每丢弃 100 条日志时,通过标准错误流(std::cerr)输出警告,提醒队列已满 */std::cerr << "Log queue full. Dropped logs count: " << dropped_logs << std::endl;}}else{/* 同步写入日志文件 */std::lock_guard<locker> lock(m_mutex);fputs(log_str.c_str(), m_fp);}
}
这里还有进一步的改进空间,比如:
- 扩容:当检测到日志队列 频繁满 的情况时,可以 动态增加队列大小。
- 优先级队列:如果日志级别非常重要(如错误日志),可以考虑不丢弃这类日志,而是选择丢弃低优先级的日志(如调试日志)。
- 使用一个 优先级队列 或通过逻辑控制,保证高优先级日志不会被丢弃。
- 内存泄漏的潜在风险:在初始化失败的情况下,某些已经分配的内存(如
m_buf
和m_log_queue
)没有被释放,可能会造成内存泄漏。
改进: 在初始化失败时和析构函数中,手动删除动态分配的内存(或者直接使用智能指针)
if (m_fp == NULL)
{std::cerr << "Failed to open log file: " << log_full_name << std::endl;delete[] m_buf;if (m_is_async) delete m_log_queue;return false; /* 初始化失败 */
}
Log::~Log()
{if (m_fp != nullptr){fflush(m_fp);fclose(m_fp);}if (m_is_async){delete m_log_queue;}delete[] m_buf;
}