参考学习:从零开始实现 C++ TinyWebServer 异步日志系统 Log类详解_tiny webserver-CSDN博客

本文用于强化学习记忆,原文讲解很清晰。

 

log.cpp:

构造函数:

Log::Log():is_async_(false), today_(0),line_count_(0),fp_(nullptr),
deque_(nullptr), write_thread_(nullptr){}
  • 作用:初始化日志类的成员变量,设置初始状态。
  • 初始化列表解析:
    • is_async_(false):默认使用同步模式(日志直接写入文件)。
    • today_(0):当前日期(初始化为 0,后续由 Init 函数设置)。
    • line_count_(0):当前日志文件的行数(从 0 开始计数)。
    • fp_(nullptr):日志文件指针(初始未打开)。
    • deque_(nullptr):阻塞队列(异步模式用,初始未创建)。
    • write_thread_(nullptr):异步写日志的线程(初始未创建)。

析构函数:

Log::~Log(){
    while(!deque_->empty())deque_->flush();//唤醒消费者处理剩下数据
    deque_->close();
    write_thread_->join();  //等待线程退出
    if(fp_){
        std::lock_guard<std::mutex> locker(mtx_);
        Flush();
        fclose(fp_);
    }
}
  • 作用:程序退出时清理资源,确保日志数据不丢失。
  • 步骤解析:
    1. 处理剩余日志:如果是异步模式(deque_ 存在),循环判断队列是否为空,调用 deque_->flush() 唤醒阻塞的写线程,处理队列中剩余的日志数据。
    2. 关闭队列:deque_->close() 通知队列 “不再接受新数据”,让写线程可以退出循环。
    3. 等待线程结束:write_thread_->join() 等待异步写线程完成,避免线程未结束就销毁资源。
    4. 关闭文件:如果文件指针 fp_ 已打开,加锁后刷新缓存(Flush()),再关闭文件(fclose(fp_)),确保数据写入磁盘。

    

初始化函数Init():

 1 //初始化
 2 void Log::Init(int level, const char* path, const char* suffix, int max_capacity){
 3     is_open_ = true;// 标记日志模块已开启(后续可通过IsOpen()判断)
 4     level_ = level;// 设置日志等级(0=DEBUG,1=INFO,...,4=FATAL)
 5     path_ = path; // 记录日志文件存放路径(如"./log")
 6     suffix_ = suffix;// 记录日志文件后缀(如".log")
 7 
 8     if(max_capacity){   // 若max_capacity>0,启用异步模式(需创建队列和线程)
 9         is_async_= true;    // 标记为异步模式
10         if(!deque_){    // 如果阻塞队列还未创建(首次初始化)
11             // 1. 创建阻塞队列(用于缓存待写日志,生产者-消费者模型)
12             std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>);
13             deque_ = std::move(new_deque);  //所有权转移    // 用移动语义转移队列所有权(避免拷贝)
14             
15             // 2. 创建异步写日志线程(线程函数为FlushLogThread)
16             std::unique_ptr<thread> new_thread(new thread(FlushLogThread));
17             write_thread_ = std::move(new_thread);   // 转移线程所有权
18         }
19     }else {
20         // 若max_capacity=0,启用同步模式(直接写文件,无需队列和线程)
21         is_async_ = false;
22     }
23     line_count_ = 0;    // 重置当前日志文件的行数计数器(从0开始)
24     path_ = path;    // 冗余:重复设置path_(和前面第3行重复,可删除)
25     suffix_ = suffix;   // 冗余:重复设置suffix_(和前面第4行重复,可删除)
26     time_t timer = time(nullptr);   // 获取当前时间(秒级,从1970年开始计算)
27     struct tm* sys_time = localtime(&timer);    // 转换为本地时间(年/月/日/时/分/秒)
28     struct tm t = *sys_time;    // 拷贝一份本地时间(避免后续操作修改原结构体)
29     char filename[Log_NAME_LENGTH] = {0};    // 定义文件名数组(长度为Log_NAME_LENGTH=256),初始化为0
30     // 格式化文件名:路径+年_月_日+后缀(例如:"./log2024_07_26.log")
31     snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%02d%s", path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
32     today_ = t.tm_mday; // 记录当前日期的“日”(如26号),用于后续判断是否跨天(需切分文件)
33 
34     {   // 作用域:限制lock_guard的生命周期(自动加锁/解锁)
35         std::lock_guard<std::mutex> locker(mtx_);   // 加锁,确保文件操作线程安全(多线程下不冲突)
36         if(fp_){    // 如果之前已经打开了日志文件
37             Flush();    // 先刷新缓存(将内存中的数据写入磁盘)
38             fclose(fp_);    // 关闭旧文件
39         }
40         fp_ = fopen(filename, "a"); // 以“追加模式”打开新日志文件(新内容加在文件末尾)
41         if(fp_ == nullptr){ // 如果文件打开失败(可能是路径不存在)
42             mkdir(path_, 0777); //777最大权限   // 创建日志目录(0777表示最大权限)
43             fp_ = fopen(filename, "a"); // 再次尝试打开文件
44         }
45         assert(fp_ != nullptr); // 断言:如果文件仍未打开,则程序终止(避免后续操作出错)
46     }
47 }

这里的std::unique_ptr表示智能指针类型,是C++11引入的独占所有权智能指针,它的核心作用是:自动管理动态资源的生命周期,再执政超出作用域时自动调用delete释放内存,避免手动new/delete导致的内存泄露(比如忘记delete,或在异常场景下delete未执行)。

这里说涉及到程序中堆和栈的问题,在栈上创建对象只要出了作用域就会自动销毁,在堆上创建对象需要手动进行销毁。

这两的区分方式主要是看作用域,创建方式,生命周期;一般栈上的创建时直接声名,不需要使用new、malloc等动态分配关键字。堆则是使用new这类的关键字。栈上创建的生命周期严格绑定作用域,出了作用域就自动销毁了。

 

根据日志等级,向日志缓冲区添加对应的等级前缀AppendLogLevel

1 void Log::AppendLogLevel(int level){
2     //定义一个存储 “日志等级前缀字符串” 的数组,每个元素对应一个等级的标识。
3     const char* level_title[] = { "[DEBUG]:","[INFO]:", "[WARN]:", "[ERROR]:", "[FATAL]:"};
4     //校验并修正日志等级
5     int valid_level = (level >= 0 && level <=4 ) ? level : 1;
6     //将等级前缀添加到缓冲区
7     buff_.Append(level_title[valid_level], 9);
8 }

 

日志核心写入函数Write:

 1 void Log::Write(int level, const char* format, ...){
 2     struct timeval now = {0, 0};    // 1. 初始化时间结构体(包含秒和微秒)
 3     gettimeofday(&now, nullptr);     // 2. 获取当前时间(精确到微秒),存到now中
 4     time_t time_second = now.tv_sec; // 3. 提取秒级时间(从1970年开始的秒数)
 5     struct tm* sys_time = localtime(&time_second);   // 4. 将秒级时间转换为本地时间(年/月/日/时/分/秒)
 6     struct tm t = *sys_time;    // 5. 拷贝本地时间到结构体t(避免sys_time被其他线程修改,确保线程安全)
 7 
 8     //日期不对或行数满了
 9     // 6. 判断是否需要切换文件:① 日期变化(跨天);② 行数达到上限(当前文件满了)
10     if(today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))){
11         // 7. 加锁后立即解锁:暂时占用锁防止其他线程同时修改文件,随后释放减少阻塞
12         std::unique_lock<std::mutex> locker(mtx_);
13         locker.unlock();
14 
15         // 8. 定义新文件名和日期后缀的缓冲区
16         char new_file[Log_NAME_LENGTH]; // 新日志文件名(长度由Log_NAME_LENGTH宏定义,如256)
17         char tail[36];  // 日期后缀(如"2024_07_26")
18 
19         // 9. 格式化日期后缀为"年_月_日"(如2024_07_26)
20         snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
21 
22         // 10. 分支1:日期变化(跨天,如从25日到26日)
23         if(today_ != t.tm_mday)
24         {
25             // 11. 新文件名格式:路径+日期+后缀(如"./log/2024_07_26.log")
26             snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_);
27             today_ = t.tm_mday; // 更新当前日期为新的"日"(如26)
28         }else{
29             // 12. 分支2:行数满了(当前文件达到MAX_LINES行)
30             int num = line_count_ / MAX_LINES;   // 计算序号(第几个文件,如1、2、3...)
31             // 13. 新文件名格式:路径+日期-序号+后缀(如"./log/2024_07_26-1.log")
32             snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_);
33         }
34 
35         // 14. 加锁处理文件切换(确保线程安全)
36         {
37             std::lock_guard<std::mutex> locker(mtx_);   // 加锁,防止多线程同时操作文件指针
38             Flush();    // 15. 刷新旧文件的缓存(将内存数据写入磁盘)
39             fclose(fp_);    // 16. 关闭旧文件
40             fp_ = fopen(new_file, "a"); // 17. 以追加模式打开新文件
41             assert(fp_ != nullptr); // 18. 断言:确保文件打开成功(失败则程序终止)
42         }
43     }
44     // 19. 加锁:确保日志内容格式化和写入的原子性(线程安全)
45     {
46         std::lock_guard<std::mutex> locker(mtx_);
47         line_count_++;  // 20. 当前文件行数+1(用于判断是否达到切换阈值)
48 
49         // 21. 格式化时间戳到缓冲区:"年-月-日 时:分:秒.微秒"(如2024-07-26 15:30:20.123456)
50         int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld",
51             t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,  // 年(+1900)、月(+1)、日
52             t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);    // 时、分、秒、微秒
53         buff_.HasWritten(n);    // 22. 更新缓冲区已写入的长度(n字节)
54         AppendLogLevel(level);  // 23. 向缓冲区添加日志等级前缀(如[DEBUG]:,调用之前分析的函数)
55 
56         // 24. 处理用户传入的可变参数(如LOG_DEBUG("fd=%d", fd)中的"fd=%d"和fd)
57         va_list vaList; // 定义可变参数列表
58         va_start(vaList, format);   // 初始化参数列表(绑定到format后面的参数)
59         // 25. 将格式化的日志内容写入缓冲区(如"fd=5")
60         int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList);
61         va_end(vaList); // 26. 结束可变参数处理(释放资源)
62         buff_.HasWritten(m);    // 27. 更新缓冲区已写入的长度(m字节)
63         buff_.Append("\n\0", 2);    // 28. 向缓冲区添加换行符和字符串终止符(每个日志占一行)
64 
65         // 29. 根据模式写入:异步模式放入队列,同步模式直接写入文件
66         if( is_async_ && deque_)
67             deque_->push_back(buff_.RetrieveAllAsString()); // 异步:缓冲区内容转字符串入队
68         else
69             fputs(buff_.ReadBegin(), fp_);  // 同步:直接将缓冲区内容写入当前文件
70         buff_.RetrieveAll();    // 30. 清空缓冲区,准备下次写入
71     }   
72 
73 }
Write 函数的完整流程是:
  1. 获取精确时间 → 2. 判断是否需要切换日志文件(按日期 / 行数)→ 3. 线程安全地切换文件 → 4. 格式化日志内容(时间 + 等级 + 用户信息)→ 5. 按模式写入(同步写文件 / 异步入队列)→ 6. 清空缓冲区。

 

GetInstance单例模式

1 //单例模式之饿汉模式
2 // 定义一个静态成员函数GetInstance,返回Log类的指针
3 Log* Log::GetInstance(){
4     //静态局部变量的初始化是线程安全的
5     // 定义一个静态局部变量log:属于Log类的唯一实例
6     static Log log;
7     // 返回这个唯一实例的地址
8     return &log;
9 }

关键 1:static Log log; —— 静态局部变量的 “唯一性”

  • 静态局部变量的生命周期:与程序一致(从第一次初始化到程序结束),而不是像普通局部变量那样随函数调用结束而销毁。
  • 初始化特性:静态局部变量只会被初始化一次(第一次调用 GetInstance() 时),后续调用该函数时,不会再创建新的 Log 对象,直接使用已有的 log 实例。
这就保证了:整个程序运行期间,Log 类只有一个实例(就是这个 static log)。

关键 2:static 成员函数 —— 全局访问点

GetInstance() 是 Log 类的静态成员函数,意味着:
  • 不需要创建 Log 类的实例,就可以直接通过 Log::GetInstance() 调用(比如 Log::GetInstance()->Write(...))。
  • 整个程序的任何地方,都能通过这个函数获取到 Log 的唯一实例,实现了 “全局访问”。

关键 3:线程安全(C++11 及以上)

代码注释提到 “静态局部变量的初始化是线程安全的”,这是 C++11 标准后的特性:
    • 当多个线程同时第一次调用 GetInstance() 时,编译器会保证 static Log log; 这行代码只执行一次(不会因为并发导致创建多个实例)。
    • 这避免了 “懒汉模式” 中需要手动加锁(如 std::mutex)才能保证线程安全的麻烦,简化了实现。

 

 

异步日志的写线程函数FlushLogThread

1 //异步日志的写线程函数
2 void Log::FlushLogThread(){
3     Log::GetInstance()->AsyncWrite();   // 核心逻辑:调用单例的AsyncWrite方法
4 }
//写线程真正的执行函数
void Log::AsyncWrite(){
    string str = "";    // 用于临时存储从队列中取出的单条日志内容
    while (deque_->pop(str)){   // 循环条件:从队列中成功取出日志(队列未关闭且有数据)
        //异步模式-消费者
        std::lock_guard<std::mutex> locker(mtx_);   // 加锁,确保文件操作线程安全
        fputs(str.c_str(), fp_);        // 将日志内容写入当前打开的文件
    }
}

 

 

剩下一些函数很容易就看懂

 1 //唤醒消费者,开始写日志
 2 void Log::Flush(){
 3     if(is_async_){   // 判断当前是否为异步日志模式
 4         deque_->flush();    // 唤醒阻塞队列中的消费者,强制处理剩余日志
 5     }
 6     fflush(fp_);    // 刷新文件指针fp_对应的流缓冲区,将内存数据写入磁盘
 7 }
 8 
 9 int Log::GetLevel(){
10     std::lock_guard<std::mutex> lock(mtx_);
11     return level_;
12 }
13 
14 void Log::SetLevel(int level){
15     std::lock_guard<std::mutex> lock(mtx_);
16     level_ = level;
17 }
18 
19 bool Log::IsOpen(){
20     return is_open_;
21 }

 

 

log.cpp

  1 #include "Log/log.h"
  2 
  3 Log::Log():is_async_(false), today_(0),line_count_(0),fp_(nullptr),
  4 deque_(nullptr), write_thread_(nullptr){}
  5 
  6 Log::~Log(){
  7     while(!deque_->empty())deque_->flush();//唤醒消费者处理剩下数据
  8     deque_->close();
  9     write_thread_->join();  //等待线程退出
 10     if(fp_){
 11         std::lock_guard<std::mutex> locker(mtx_);
 12         Flush();
 13         fclose(fp_);
 14     }
 15 }
 16 
 17 //初始化
 18 void Log::Init(int level, const char* path, const char* suffix, int max_capacity){
 19     is_open_ = true;// 标记日志模块已开启(后续可通过IsOpen()判断)
 20     level_ = level;// 设置日志等级(0=DEBUG,1=INFO,...,4=FATAL)
 21     path_ = path; // 记录日志文件存放路径(如"./log")
 22     suffix_ = suffix;// 记录日志文件后缀(如".log")
 23 
 24     if(max_capacity){   // 若max_capacity>0,启用异步模式(需创建队列和线程)
 25         is_async_= true;    // 标记为异步模式
 26         if(!deque_){    // 如果阻塞队列还未创建(首次初始化)
 27             // 1. 创建阻塞队列(用于缓存待写日志,生产者-消费者模型)
 28             std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>);
 29             deque_ = std::move(new_deque);  //所有权转移    // 用移动语义转移队列所有权(避免拷贝)
 30             
 31             // 2. 创建异步写日志线程(线程函数为FlushLogThread)
 32             std::unique_ptr<thread> new_thread(new thread(FlushLogThread));
 33             write_thread_ = std::move(new_thread);   // 转移线程所有权
 34         }
 35     }else {
 36         // 若max_capacity=0,启用同步模式(直接写文件,无需队列和线程)
 37         is_async_ = false;
 38     }
 39     line_count_ = 0;    // 重置当前日志文件的行数计数器(从0开始)
 40     path_ = path;    // 冗余:重复设置path_(和前面第3行重复,可删除)
 41     suffix_ = suffix;   // 冗余:重复设置suffix_(和前面第4行重复,可删除)
 42     time_t timer = time(nullptr);   // 获取当前时间(秒级,从1970年开始计算)
 43     struct tm* sys_time = localtime(&timer);    // 转换为本地时间(年/月/日/时/分/秒)
 44     struct tm t = *sys_time;    // 拷贝一份本地时间(避免后续操作修改原结构体)
 45     char filename[Log_NAME_LENGTH] = {0};    // 定义文件名数组(长度为Log_NAME_LENGTH=256),初始化为0
 46     // 格式化文件名:路径+年_月_日+后缀(例如:"./log2024_07_26.log")
 47     snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%02d%s", path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
 48     today_ = t.tm_mday; // 记录当前日期的“日”(如26号),用于后续判断是否跨天(需切分文件)
 49 
 50     {   // 作用域:限制lock_guard的生命周期(自动加锁/解锁)
 51         std::lock_guard<std::mutex> locker(mtx_);   // 加锁,确保文件操作线程安全(多线程下不冲突)
 52         if(fp_){    // 如果之前已经打开了日志文件
 53             Flush();    // 先刷新缓存(将内存中的数据写入磁盘)
 54             fclose(fp_);    // 关闭旧文件
 55         }
 56         fp_ = fopen(filename, "a"); // 以“追加模式”打开新日志文件(新内容加在文件末尾)
 57         if(fp_ == nullptr){ // 如果文件打开失败(可能是路径不存在)
 58             mkdir(path_, 0777); //777最大权限   // 创建日志目录(0777表示最大权限)
 59             fp_ = fopen(filename, "a"); // 再次尝试打开文件
 60         }
 61         assert(fp_ != nullptr); // 断言:如果文件仍未打开,则程序终止(避免后续操作出错)
 62     }
 63 }
 64 
 65 
 66 void Log::AppendLogLevel(int level){
 67     //定义一个存储 “日志等级前缀字符串” 的数组,每个元素对应一个等级的标识。
 68     const char* level_title[] = { "[DEBUG]:","[INFO]:", "[WARN]:", "[ERROR]:", "[FATAL]:"};
 69     //校验并修正日志等级
 70     int valid_level = (level >= 0 && level <=4 ) ? level : 1;
 71     //将等级前缀添加到缓冲区
 72     buff_.Append(level_title[valid_level], 9);
 73 }
 74 
 75 void Log::Write(int level, const char* format, ...){
 76     struct timeval now = {0, 0};    // 1. 初始化时间结构体(包含秒和微秒)
 77     gettimeofday(&now, nullptr);     // 2. 获取当前时间(精确到微秒),存到now中
 78     time_t time_second = now.tv_sec; // 3. 提取秒级时间(从1970年开始的秒数)
 79     struct tm* sys_time = localtime(&time_second);   // 4. 将秒级时间转换为本地时间(年/月/日/时/分/秒)
 80     struct tm t = *sys_time;    // 5. 拷贝本地时间到结构体t(避免sys_time被其他线程修改,确保线程安全)
 81 
 82     //日期不对或行数满了
 83     // 6. 判断是否需要切换文件:① 日期变化(跨天);② 行数达到上限(当前文件满了)
 84     if(today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))){
 85         // 7. 加锁后立即解锁:暂时占用锁防止其他线程同时修改文件,随后释放减少阻塞
 86         std::unique_lock<std::mutex> locker(mtx_);
 87         locker.unlock();
 88 
 89         // 8. 定义新文件名和日期后缀的缓冲区
 90         char new_file[Log_NAME_LENGTH]; // 新日志文件名(长度由Log_NAME_LENGTH宏定义,如256)
 91         char tail[36];  // 日期后缀(如"2024_07_26")
 92 
 93         // 9. 格式化日期后缀为"年_月_日"(如2024_07_26)
 94         snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
 95 
 96         // 10. 分支1:日期变化(跨天,如从25日到26日)
 97         if(today_ != t.tm_mday)
 98         {
 99             // 11. 新文件名格式:路径+日期+后缀(如"./log/2024_07_26.log")
100             snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_);
101             today_ = t.tm_mday; // 更新当前日期为新的"日"(如26)
102         }else{
103             // 12. 分支2:行数满了(当前文件达到MAX_LINES行)
104             int num = line_count_ / MAX_LINES;   // 计算序号(第几个文件,如1、2、3...)
105             // 13. 新文件名格式:路径+日期-序号+后缀(如"./log/2024_07_26-1.log")
106             snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_);
107         }
108 
109         // 14. 加锁处理文件切换(确保线程安全)
110         {
111             std::lock_guard<std::mutex> locker(mtx_);   // 加锁,防止多线程同时操作文件指针
112             Flush();    // 15. 刷新旧文件的缓存(将内存数据写入磁盘)
113             fclose(fp_);    // 16. 关闭旧文件
114             fp_ = fopen(new_file, "a"); // 17. 以追加模式打开新文件
115             assert(fp_ != nullptr); // 18. 断言:确保文件打开成功(失败则程序终止)
116         }
117     }
118     // 19. 加锁:确保日志内容格式化和写入的原子性(线程安全)
119     {
120         std::lock_guard<std::mutex> locker(mtx_);
121         line_count_++;  // 20. 当前文件行数+1(用于判断是否达到切换阈值)
122 
123         // 21. 格式化时间戳到缓冲区:"年-月-日 时:分:秒.微秒"(如2024-07-26 15:30:20.123456)
124         int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld",
125             t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,  // 年(+1900)、月(+1)、日
126             t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);    // 时、分、秒、微秒
127         buff_.HasWritten(n);    // 22. 更新缓冲区已写入的长度(n字节)
128         AppendLogLevel(level);  // 23. 向缓冲区添加日志等级前缀(如[DEBUG]:,调用之前分析的函数)
129 
130         // 24. 处理用户传入的可变参数(如LOG_DEBUG("fd=%d", fd)中的"fd=%d"和fd)
131         va_list vaList; // 定义可变参数列表
132         va_start(vaList, format);   // 初始化参数列表(绑定到format后面的参数)
133         // 25. 将格式化的日志内容写入缓冲区(如"fd=5")
134         int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList);
135         va_end(vaList); // 26. 结束可变参数处理(释放资源)
136         buff_.HasWritten(m);    // 27. 更新缓冲区已写入的长度(m字节)
137         buff_.Append("\n\0", 2);    // 28. 向缓冲区添加换行符和字符串终止符(每个日志占一行)
138 
139         // 29. 根据模式写入:异步模式放入队列,同步模式直接写入文件
140         if( is_async_ && deque_)
141             deque_->push_back(buff_.RetrieveAllAsString()); // 异步:缓冲区内容转字符串入队
142         else
143             fputs(buff_.ReadBegin(), fp_);  // 同步:直接将缓冲区内容写入当前文件
144         buff_.RetrieveAll();    // 30. 清空缓冲区,准备下次写入
145     }   
146 
147 }
148 
149 
150 //单例模式之饿汉模式
151 // 定义一个静态成员函数GetInstance,返回Log类的指针
152 Log* Log::GetInstance(){
153     //静态局部变量的初始化是线程安全的
154     // 定义一个静态局部变量log:属于Log类的唯一实例
155     static Log log;
156     // 返回这个唯一实例的地址
157     return &log;
158 }
159 
160 
161 
162 //异步日志的写线程函数
163 void Log::FlushLogThread(){
164     Log::GetInstance()->AsyncWrite();   // 核心逻辑:调用单例的AsyncWrite方法
165 }
166 
167 //写线程真正的执行函数
168 void Log::AsyncWrite(){
169     string str = "";    // 用于临时存储从队列中取出的单条日志内容
170     while (deque_->pop(str)){   // 循环条件:从队列中成功取出日志(队列未关闭且有数据)
171         //异步模式-消费者
172         std::lock_guard<std::mutex> locker(mtx_);   // 加锁,确保文件操作线程安全
173         fputs(str.c_str(), fp_);        // 将日志内容写入当前打开的文件
174     }
175 }
176 
177 //唤醒消费者,开始写日志
178 void Log::Flush(){
179     if(is_async_){   // 判断当前是否为异步日志模式
180         deque_->flush();    // 唤醒阻塞队列中的消费者,强制处理剩余日志
181     }
182     fflush(fp_);    // 刷新文件指针fp_对应的流缓冲区,将内存数据写入磁盘
183 }
184 
185 int Log::GetLevel(){
186     std::lock_guard<std::mutex> lock(mtx_);
187     return level_;
188 }
189 
190 void Log::SetLevel(int level){
191     std::lock_guard<std::mutex> lock(mtx_);
192     level_ = level;
193 }
194 
195 bool Log::IsOpen(){
196     return is_open_;
197 }

 

 

还有对于log.cpp的测试源文件,这里不详细剖析了:

 1 #include "Log/log.h"
 2 #include <iostream>
 3 
 4 int main(){
 5     //获取Log类的单例实例
 6     Log* logger = Log::GetInstance();
 7 
 8     //初始化日志系统
 9     logger->Init(0, "./logs/", ".log", 1024);
10 
11     //输出不同级别的日志信息
12     LOG_DEBUG("This is a debug message.");
13     LOG_INFO("This is an info message.");
14     LOG_WARN("This is a warning message.");
15     LOG_ERROR("This is an error message.");
16     LOG_FATAL("This is an fatal message.");
17 
18     //输出日志级别
19     std::cout << "Current log level: " << logger->GetLevel() << std::endl;
20 
21     //修改日志级别
22     logger->SetLevel(2);
23     std::cout << "New log level: " << logger->GetLevel() << std::endl;
24 
25     //再次输出不同级别的日志信息
26     LOG_DEBUG("This debug message should not be logged.");
27     LOG_INFO("This info message should not be logged.");
28     LOG_WARN("This is a new warning message.");
29     LOG_ERROR("This is a new error message.");
30     LOG_FATAL("This is a new fatal message.");
31 
32     //可变参数
33     logger->SetLevel(0);
34     LOG_DEBUG("%s %d %d", "log info", 123, 666);
35 
36     //大量日志
37     for(int i = 0; i < 100000; ++i)
38     LOG_DEBUG("This is the %d-th errpr message.", i);
39 
40     return 0;
41 }