01 背景

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

(2)因此设计了一个线程安全的消息队列(主要采用了互斥锁和条件变量),写日志的时候会先将日志信息放到消息队列中去,再有专门的写日志进程读取消息队列中的日志,写入文件中。

(3)最后,设计了日志模块设计成为了单例模式。

02 日志框架设计

异步缓冲日志队列

grpc 日志 请求参数_rpc

03 单例模式设计

单例模式就是一个类只允许创建出一个实例对象。

单例模式的好处主要有两个:

(1) 可以用来解决资源冲突,比如日志模块, 假设两个对象同时写入日志文件或者对共享变量执行修改,就会出现相互覆盖的情况,而单例模式只会产生一个对象,这个对象统一访问共享资源或者竞争资源,可以避免相互覆盖的情况

(2)第二表示全局唯一类,有些数据在系统中只应该保留一份,所以应该设计为单例模式,比如配置文件、全局ID生成器

饿汉式单例模式

一开始就加载了实例

#include <iostream>

using namespace std;
//饿汉式单例模式
class Singleton {
public:
    static Singleton* Getinstance() {
        static Singleton instance;
        return &instance;
    }
private:
    Singleton() {
        cout << "Singleton mode" << endl;
    }
};

int main() {
    Singleton* test1 = Singleton::Getinstance();
    Singleton* test2 = Singleton::Getinstance();
    system("pause");
    return 0;
}

grpc 日志 请求参数_单例模式_02

这种写法在C++98中不是线程安全的,但是在C++11起是线程安全的,因为静态的局部变量在调用的时候分配到静态存储区,所以在编译的时候没有分配。

静态局部对象:
在程序执行到该对象的定义处时,创建对象并调用相应的构造函数!
如果在定义对象时没有提供初始指,则会暗中调用默认构造函数,如果没有默认构造函数,则自动初始化为0。
如果在定义对象时提供了初始值,则会暗中调用类型匹配的带参的构造函数(包括拷贝构造函数),如果没有定义这样的构造函数,编译器可能报错!
直到main()结束后才会调用析构函数!

懒汉式单例模式

#include <iostream>

using namespace std;

//懒汉式单例模式
class Singleton {
public:
    static Singleton* Getinstance() {
        if (instance == nullptr) {
            instance = new Singleton;
        }
        return instance;
    }
private:
    Singleton() {
        cout << "Singleton mode" << endl;
    }
    static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;  // 这一步很重要!!!!

int main() {
    Singleton* test1 = Singleton::Getinstance();
    Singleton* test2 = Singleton::Getinstance();
    system("pause");
    return 0;
}
#endif

特点是延迟加载 当函数被首次访问的时候才加载

线程安全的懒汉式单例模式

#include <iostream>
#include <mutex>

using namespace std;

//线程安全懒汉式单例模式
class Singleton {
public:
    static Singleton* Getinstance() {
        if (instance == nullptr) {
            lock_guard<mutex> lc(mtx);
            instance = new Singleton;
        }
        return instance;
    }
private:
    Singleton() {
        cout << "Singleton mode" << endl;
    }
    static Singleton* instance;
    static mutex mtx;
};

mutex Singleton::mtx;
Singleton* Singleton::instance;

int main() {
    Singleton* test1 = Singleton::Getinstance();
    Singleton* test2 = Singleton::Getinstance();
    system("pause");
    return 0;
}

grpc 日志 请求参数_分布式_03

04 日志模块源码解析

(1)线程安全的队列 借助mutex和cv实现了queue

(2)日志模块本身设计为(饿汉式)单例模式

(3)最后,主要实现了两部分功能 一个是rpc服务端worker线程向队列中写入数据 主要利用push接口 log函数实现 然后是 加入写日志线程 向日志文件中写队列的数据 主要利用pop接口(设置分离线程)

lockqueue.h

(1)首先需要封装一个日志队列的自定义的push和pop的api接口,通过mutex和conditional_variable来保证线程安全。

(2)他的原理类似一个生产者消费者模型,队列push函数处理的是rpc服务器端的多个worker线程向队列里写数据,写之前加上一把互斥锁,然后push数据,结束以后notify阻塞等待写日志线程向磁盘写数据。

(3)pop接口 首先会检测队列是否为空 为空代表没有数据 就会进入阻塞wait状态 然后释放锁 ,有数据来了返回数据。

#pragma once
#include <queue>
#include <thread>
#include <mutex>    // pthread_mutex_t  线程互斥
#include <condition_variable>   // pthread_condition_t   线程通信


// 模板代码  不能写在cc文件中
// 异步写日志的日志队列
template<typename T> 
class LockQueue {
public:
    //muduo 提供的 多个worker线程都会写日志queue
    void Push(const T &data) {
        std::lock_guard<std::mutex> lock(m_mutex);  // 获得互斥锁
        m_queue.push(data);
        m_condvariable.notify_one();  // 唤醒wait线程
    }
    // 出右括号释放锁

    // 一个线程 在读日志queue,写日志文件
    T Pop() {
        std::unique_lock<std::mutex> lock(m_mutex);
        while (m_queue.empty()) {
            // 日志队列为空, 线程进入wait状态
            m_condvariable.wait(lock);  // 进入wait等待状态  释放锁
        }

        T data = m_queue.front();
        m_queue.pop();
        return data;
    }
private:
    std::queue<T> m_queue;
    std::mutex m_mutex;
    std::condition_variable m_condvariable;
};

logger.h

#pragma once
#include "lockqueue.h"
#include <string>

enum LogLevel {
    INFO,   // 普通信息
    ERROR,  // 错误信息
};

//mprpc框架提供的日志系统

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;
};

//定义宏    可变参 LOG_INFO("xxx %d %s", 20, "xxx")
#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)

logger.cc

#include "logger.h"
#include "time.h"
#include <iostream>

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();
}

//设置日志级别
void Logger::SetLogLevel(LogLevel level) {
    m_loglevel = level;
}


// 写日志, 把日志信息写入lockqueue缓冲区中
void Logger::Log(std::string msg) {
    //
    m_lckQue.Push(msg);   // 从worker线程中把数据写入queue
}

05 宏的写法

//定义宏    可变参 LOG_INFO("xxx %d %s", 20, "xxx")
#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)
  • 反斜杠\表示 继续符 本行与下一行连接起来

_VA_ARGS_的用法

自定义打印时,用到可变参数,用...即可表示可变参数,如下:

#include <stdio.h>

#define LOG1(...)               printf(__VA_ARGS__)//...表示可变参数,__VA_ARGS__就是将...的值复制到这里
int main(int argc, char** argv)
{
    char *str = "test __VA_ARGS__";
    int num = 10086;
    LOG1("this is test __VA_ARGS__\r\n");
    LOG1("this is test __VA_ARGS__:%s, %d\r\n", str, num);

    return 0;
}

打印结果如下:

this is test __VA_ARGS__
this is test __VA_ARGS__:test __VA_ARGS__, 10086

VA_ARGS__就是将…的值复制到这里

int main(int argc, char** argv)
 {
 char *str = “test VA_ARGS”;
 int num = 10086;
 LOG1(“this is test VA_ARGS\r\n”);
 LOG1(“this is test VA_ARGS:%s, %d\r\n”, str, num);return 0;}
打印结果如下:

```cpp
this is test __VA_ARGS__
this is test __VA_ARGS__:test __VA_ARGS__, 10086