五、日志模块原理与实现 by TEnth丶

491 阅读6分钟

5.1 单例模式

引用定义:

确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

从定义中可以看出,单例模式有以下几个重点:

  • 这个类只能有一个实例
  • 它必须自己创建这个实例
  • 它必须自己向整个系统提供这个实例

webserverna.png

懒汉模式和饿汉模式

懒汉模式:懒加载模式

  • 优点:节省资源,在需要的时候创建对象。

  • 缺点:线程不安全。获取对象的时候,效率低

  • 最简单的线程安全的方式:同步方法,效率低

  • 更好的的线程安全的方式:双重校验锁

    源码:

    //懒汉模式 + 锁
    class singletonLazy{
        singletonLazy(){};
        ~singletonLazy(){};
    public:
        static singletonLazy* getInstance(){
            if(instance == nullptr){
                mutex_t.lock();
                if(instance == nullptr){
                    instance = new singletonLazy();
                }
            }
            return instance;
        }
    private:
        static singletonLazy* instance;
        static mutex mutex_t;
    };
    singletonLazy* singletonLazy::instance = nullptr;
    mutex singletonLazy::mutex_t;
    

饿汉模式:预加载模式

前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。并且C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁,其中C++0x是C++11标准成为正式标准之前的草案临时名字。

  • 优点:在类加载的时候,就创建好对象放到静态区了,获取对象效率高。线程安全

  • 缺点:类加载效率低,并且static修饰的成员占用资源。

  • 隐藏的问题:在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

    源码:

    //饿汉模式
    class singletonHungry{
        singletonHungry(){};
        ~singletonHungry(){};
    public:
        static singletonHungry* getInstance(){
            return instance;
        }
    private:
        static singletonHungry* instance;
    };
    singletonHungry* singletonHungry::instance = new singletonHungry();
    ​
    //或者这样写,这种模式详解见《【杂记】踩坑——为什么单例模式之饿汉模式实现可以不在类外初始化静态变量?》
    class singletonHungry{
        singletonHungry(){};
        ~singletonHungry(){};
    public:
        //直接初始化这个静态变量并返回它的地址
        static singletonHungry* getInstance(){
            static singletonHungry instance;
            return &instance;
        }
    private:
        static singletonHungry* instance;
    };
    

由于日志文件大概率会用到,并且为了提高写日志的效率,这里采用饿汉模式。

5.2 生产者消费者模型

webserver5.2a.png

下面代码是来自《UNIX环境高级编程 第2版》中第11章线程关于pthread_cond_wait的介绍中有一个生产者-消费者的例子

生产者和消费者是互斥关系,两者对缓冲区访问互斥,同时生产者和消费者又是一个相互协作与同步的关系,只有生产者生产之后,消费者才能消费。

#include <pthread.h>
struct msg {
  struct msg *m_next;
  /* value...*/
};
​
struct msg* workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
​
void process_msg() {
  struct msg* mp;
  for (;;) {
    pthread_mutex_lock(&qlock);
    //这里需要用while,而不是if
    while (workq == NULL) {
      pthread_cond_wait(&qread, &qlock);
    }
    mq = workq;
    workq = mp->m_next;
    pthread_mutex_unlock(&qlock);
    /* now process the message mp */
  }
}
​
void enqueue_msg(struct msg* mp) {
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    /** 此时另外一个线程在signal之前,执行了process_msg,刚好把mp元素拿走*/
    pthread_cond_signal(&qready);
    /** 此时执行signal, 在pthread_cond_wait等待的线程被唤醒,
        但是mp元素已经被另外一个线程拿走,所以,workq还是NULL ,因此需要继续等待*/
}
​

条件变量

pthread_cond_wait函数,用于等待目标条件变量。该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,表示重新抢到了互斥锁,互斥锁会再次被锁上, 也就是说函数内部会有一次解锁和加锁操作.

pthread_cond_wait执行后的内部操作分为以下几步:

  • 将线程放在条件变量的请求队列后,内部解锁
  • 线程等待被pthread_cond_broadcast信号唤醒或者pthread_cond_signal信号唤醒,唤醒后去竞争锁
  • 若竞争到互斥锁,内部再次加锁

详见《【杂记】转载:Linux条件变量pthread_condition细节(为何先加锁,pthread_cond_wait为何先解锁,返回时又加锁)》

封装为阻塞队列

阻塞队列类中封装了生产者-消费者模型,其中push成员是生产者,pop成员是消费者

阻塞队列中,使用了循环数组实现了队列,作为两者共享缓冲区,当然了,队列也可以使用STL中的queue。

直接来看一下poppush的源码:

template <class T>
class block_queue
{
public:
    block_queue(int max_size = 1000)
    {
        //assert(max_size > 0);
        if (max_size <= 0)
        {
            exit(-1);
        }
​
        m_max_size = max_size;
        m_array = new T[max_size];
        m_size = 0;
        m_front = -1;
        m_back = -1;
    }
    //其他类函数
    ......
private:
    //提前封装为类,以实现RAII,详见《一、锁机制原理与实现 by TEnth丶》
    locker m_mutex;
    cond m_cond;
​
    T *m_array;
    int m_size;
    int m_max_size;
    int m_front;
    int m_back;
};
​
//pop时,如果当前队列没有元素,将会等待条件变量
    bool pop(T &item)
    {
        m_mutex.lock();
        //多个消费者的时候,这里要是用while而不是if
        while (m_size <= 0)
        {
            //当重新抢到互斥锁,pthread_cond_wait返回为0
            if (!m_cond.wait(m_mutex.get()))
            {
                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;
    }
​
    //往队列添加元素,需要将所有使用队列的线程先唤醒
    //当有元素push进队列,相当于生产者生产了一个元素
    //若当前没有线程等待条件变量,则唤醒无意义
    bool push(const T &item)
    {
​
        m_mutex.lock();
        if (m_size >= m_max_size)
        {
            m_cond.broadcast();
            m_mutex.unlock();
            return false;
        }
        //将新增数据放在循环数组的对应位置
        m_back = (m_back + 1) % m_max_size;
        m_array[m_back] = item;
​
        m_size++;
​
        m_cond.broadcast();
        m_mutex.unlock();
        return true;
    }

5.3 日志写原理及实现

项目实现了两种写日志的方法:同步、异步。其中,异步显然是更好的选择:

  • 所谓同步日志,即当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句。
  • 使用异步日志进行输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,处理业务逻辑的主线程不用等待即可执行后续业务逻辑。

流程图

webserver5.3a.png

(图片出处:@两猿社)

  • 日志文件
    • 局部变量的懒汉模式获取实例
    • 生成日志文件,并判断同步和异步写入方式
  • 同步
    • 判断是否分文件
    • 直接格式化输出内容,将信息写入日志文件
  • 异步
    • 判断是否分文件
    • 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

基础API

fputs

#include <stdio.h>
int fputs(const char *str, FILE *stream);
  • str,一个数组,包含了要写入的以空字符终止的字符序列。
  • stream,指向FILE对象的指针,该FILE对象标识了要被写入字符串的流。

可变参数宏VA_ARGS****

VA_ARGS是一个可变参数的宏,定义时宏定义中参数列表的最后一个参数为省略号,在实际使用时会发现有时会加##,有时又不加。

1//最简单的定义
2#define my_print1(...)  printf(__VA_ARGS__)
3
4//搭配va_list的format使用
5#define my_print2(format, ...) printf(format, __VA_ARGS__)  
6#define my_print3(format, ...) printf(format, ##__VA_ARGS__)

fflush

#include <stdio.h>
int fflush(FILE *stream);

fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中,如果参数stream 为NULL,fflush()会将所有打开的文件数据更新。

在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。

在prinf()后加上fflush(stdout); 强制马上输出到控制台,可以避免出现上述错误。