RPC分布式网络通信框架(四)—— 异步日志模块设计

文章目录

  • 异步日志模块
    • Logger类实现
    • 线程安全LockQueue类实现


异步日志模块

问题:由于RPC服务器端采用了epoll+多线程 ,并发处理来自客户端的请求,所以有可能造成多线程同时写日志信息。

将日志信息写入一个queue中,然后新建日志线程。但是需要注意的是,由于rpcprovider类中是epoll+多线程,所以进而会创建多个日志线程。这样必须要保证线程安全
RPC分布式网络通信框架(四)—— 异步日志模块设计_第1张图片
于是需要自定义线程安全的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);
}

宏定义体中的具体操作如下:

  1. 通过调用 Logger::GetInstance() 来获取 Logger 类的单例实例
  2. 设置日志级别为 INFO/ERROR,通过调用 logger.SetLogLevel(INFO/ERROR)。
  3. 创建一个大小为 1024 字节的字符数组 c,并将其初始化为全零。
  4. 使用 snprintf 函数将格式化的日志消息和可变数量的参数写入字符数组 c 中,最多写入 1024 字节。
  5. 调用 logger.Log(c) 将字符数组中消息塞入日志系统中的lockqueue中。
  6. 整个宏定义通过 do-while 语句实现了一个代码块,目的是确保宏定义可以像普通语句一样使用,而不会受限于语法结构。while(0) 是为了确保该宏只能作为单个语句使用,并且在使用时不会引入额外的控制流。

调用方法

LOG_INFO("This is an info message: %s", message);

线程安全LockQueue类实现

  1. 首先需要封装一个日志队列的自定义的Push和Pop的api接口,通过mutex和conditional_variable来保证线程安全。
  2. 他的原理类似一个生产者消费者模型,队列Push函数处理的是rpc服务器端的多个worker线程向队列里写数据,写之前加上一把互斥锁,然后push数据,结束以后notify阻塞等待写日志线程向磁盘写数据。
  3. 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()方法来唤醒等待的线程。

你可能感兴趣的:(rpc,分布式,网络协议)