日志系统(最新版)

基础知识

日志,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。

同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

生产者-消费者模型,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。

阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。

异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。

单例模式,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法。

整体概述

本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。

其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。

日志系统大致可以分成两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

本文内容

本篇将介绍单例模式与阻塞队列的定义,具体的涉及到单例模式、生产者-消费者模型,阻塞队列的代码实现。

单例模式,描述懒汉与饿汉两种单例模式,并结合线程安全进行讨论。

生产者-消费者模型,描述条件变量,基于该同步机制实现简单的生产者-消费者模型。

代码实现,结合代码对阻塞队列的设计进行详解。

单例模式

单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。

#include <pthread.h>

// 单例类,确保整个程序只有一个实例
class single {
private:
    // 私有静态指针变量指向唯一的实例
    static single* p;
    // 静态互斥锁,用于线程安全的实例创建
    static pthread_mutex_t lock;

    // 私有化构造函数,防止外部创建实例
    single() {
        // 初始化互斥锁
        pthread_mutex_init(&lock, nullptr);
    }
    // 私有化析构函数,避免外部删除实例
    ~single() {}

public:
    // 公有静态方法,用于获取单例实例
    static single* getinstance();
};

// 定义静态互斥锁变量
pthread_mutex_t single::lock;

// 静态实例指针初始化为nullptr
single* single::p = nullptr;

/**
 * 获取单例实例的方法
 * 该方法确保在多线程环境下安全地创建单例对象
 * 返回值:单例对象指针
 */
single* single::getinstance() {
    // 如果实例尚未创建
    if (nullptr == p) {
        // 加锁,防止多线程同时创建实例
        pthread_mutex_lock(&lock);
        // 再次检查实例是否创建,避免不必要的创建
        if (nullptr == p) {
            // 实例化单例对象
            p = new single;
        }
        // 解锁
        pthread_mutex_unlock(&lock);
    }
    // 返回单例实例
    return p;
}

为什么要用双检测,只检测一次不行吗?

如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

局部静态变量之线程安全懒汉模式

前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。

// 单例类,防止外部实例化和删除
class single {
private:
    // 私有构造函数,防止外部实例化
    single() {}
    
    // 私有析构函数,防止外部delete
    ~single() {}

public:
    // 获取单例对象的静态方法
    static single* getinstance();


};

// 获取单例对象的实现
single* single::getinstance() {
    // 静态局部变量,保证其只被实例化一次
    static single obj;
    // 返回单例对象的地址
    return &obj;
}

这时候有人说了,这种方法不加锁会不会造成线程安全问题?

其实,C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁,其中C++0x是C++11标准成为正式标准之前的草案临时名字。

所以,如果使用C++11之前的标准,还是需要加锁,这里同样给出加锁的版本。

// 单例类single,确保整个程序中只有一个实例
class single {
private:
    // 静态互斥锁,用于线程安全的实例化
    static pthread_mutex_t lock;
    
    // 私有构造函数,防止外部直接构造对象
    single() {
        // 初始化互斥锁
        pthread_mutex_init(&lock, nullptr);
    }
    
    // 私有析构函数,避免对象被删除
    ~single(){}

public:
    // 获取单例对象的静态方法
    static single* getinstance();
};

// 静态互斥锁变量
pthread_mutex_t single::lock;

// 单例模式的线程安全实例化方法
single* single::getinstance() {
    // 加锁,保证线程安全
    pthread_mutex_lock(&lock);
    // 静态局部变量,保证只在第一次调用时初始化
    static single obj;
    // 解锁
    pthread_mutex_unlock(&lock);
    // 返回单例对象的地址
    return &obj;
}
饿汉模式

饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。

#include <iostream>
using namespace std;

// 定义一个单例类
class single{
private:
    // 静态成员指针,用于存储单例对象的唯一实例
    static single* p;
    // 私有构造函数,防止外部直接创建对象
    single() {}
    // 私有析构函数,防止外部删除对象
    ~single() {} 
public: 
    // 静态方法,返回单例对象的指针
    static single* getinstance();
};

// 静态成员变量初始化,创建并初始化单例对象
single* single::p = new single();

// 获取单例对象的指针方法
single* single::getinstance() {
    return p;
}

int main() {
    // 获取单例对象的指针
    single* p1 = single::getinstance();
    // 再次获取单例对象的指针
    single* p2 = single::getinstance();
    // 检查两次获取的指针是否相同,验证单例特性
    if (p1 == p2) {
        cout << "same" << endl;
    }
    return 0;
}

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

  • 懒汉模式 在第一次调用 getInstance() 时才会实例化对象,适用于对资源敏感的场景,但需要处理线程安全问题。
  • 饿汉模式 在类加载时就创建对象,无需考虑线程安全问题,适用于资源不敏感且确保对象一定会被使用的场景。

条件变量与生产者-消费者模型

条件变量API与陷阱

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。

基础API
  • pthread_cond_init函数,用于初始化条件变量
  • pthread_cond_destory函数,销毁条件变量
  • pthread_cond_broadcast函数,以广播的方式唤醒所有等待目标条件变量的线程
  • pthread_cond_wait函数,用于等待目标条件变量。该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,表示重新抢到了互斥锁,互斥锁会再次被锁上, 也就是说函数内部会有一次解锁和加锁操作.

使用pthread_cond_wait方式如下:

pthread_mutex_lock(&mutex)

while(线程执行条件是否成立) {
    pthread_cond_wait(&cond, &mutex);
}

pthread_mutex_unlock(&mutex);

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

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

使用前要加锁,为什么要加锁?

多线程访问,为了避免资源竞争,所以要加锁,使得每个线程互斥的访问公有资源。

pthread_cond_wait内部为什么要解锁?

如果while或者if判断的时候,满足执行条件,线程便会调用pthread_cond_wait阻塞自己,此时它还在持有锁,如果他不解锁,那么其他线程将会无法访问公有资源。

具体到pthread_cond_wait的内部实现,当pthread_cond_wait被调用线程阻塞的时候,pthread_cond_wait会自动释放互斥锁。

为什么要把调用线程放入条件变量的请求队列后再解锁?

线程是并发执行的,如果在把调用线程A放在等待队列之前,就释放了互斥锁,这就意味着其他线程比如线程B可以获得互斥锁去访问公有资源,这时候线程A所等待的条件改变了,但是它没有被放在等待队列上,导致A忽略了等待条件被满足的信号。

倘若在线程A调用pthread_cond_wait开始,到把A放在等待队列的过程中,都持有互斥锁,其他线程无法得到互斥锁,就不能改变公有资源。

为什么最后还要加锁?

将线程放在条件变量的请求队列后,将其解锁,此时等待被唤醒,若成功竞争到互斥锁,再次加锁。

陷阱二

为什么判断线程执行的条件用while而不是if?

一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用,便调用pthread_cond_wait,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal发送一个资源可用信号。

在wait成功之后,资源就一定可以被使用么?答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait返回后,资源可能已经被使用了。

再具体点,有可能多个线程都在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A,B两个线程都在等待,B比较速度快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但A回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继续等待下去的条件就是使用while,要不然使用if的话pthread_cond_wait返回后,就会顺序执行下去。

所以,在这种情况下,应该使用while而不是if:

while(resource == FALSE) {
    pthread_cond_wait(&cond, &mutex);
}

如果只有一个消费者,那么使用if是可以的

生产者-消费者模型

这里摘抄《Unix 环境高级编程》中第11章线程关于pthread_cond_wait的介绍中有一个生产者-消费者的例子P311,其中,process_msg相当于消费者,enqueue_msg相当于生产者,struct msg* workq作为缓冲队列。

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

生产者-消费者模型的优点

1)解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。

2)复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展。

3)调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度。

4)异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put() 的时间比较短,而支持高并发。

5)支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉。

#include <pthread.h>

// 消息结构体定义,用于在队列中传递消息
struct msg
{
    struct msg* m_next; // 指向下一个消息的指针
    // value.. // 这里可以存储消息的具体内容
};

// 工作队列的全局变量,头指针指向队列中的第一个消息
struct msg* workq;
// 初始化条件变量,用于在队列有消息时通知消费者线程
pthread_cond_t qread = PTHREAD_COND_INITIALIZER;
// 初始化互斥锁,用于保护工作队列的线程安全
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;


/**
 * 处理消息的函数
 * 
 * 本函数旨在从一个消息队列中不断取出消息并处理之
 * 它通过线程互斥量(qlock)来保证消息队列的访问是线程安全的
 * 同时,它使用条件变量(qread)来等待新消息的到达
 */
void process_msg() {
    // 指向当前正在处理的消息
    struct msg* mp;

    // 持续等待并处理消息
    for (;;) {
        // 加锁以保护共享资源
        pthread_mutex_lock(&qlock);

        // 如果队列为空,则解锁并等待新消息
        // 这里需要用while,而不是if,以防止虚假唤醒
        while (workq == nullptr) {
            pthread_cond_wait(&qread, &qlock);
        }

        // 取出队列头部的消息
        mp = workq;
        // 从队列中移除已取出的消息
        workq = mp->m_next;

        // 解锁以允许其他线程访问队列
        pthread_mutex_unlock(&qlock);

        // 此处应处理消息mp
        // 注意:具体的消息处理逻辑根据实际需求实现,此处省略
    }
}

/**
 * 将消息加入队列的函数
 * @param mp 指向要加入队列的消息的指针
 */
void enqueue_msg(struct msg* mp) {
    // 加锁保护工作队列
    pthread_mutex_lock(&qlock);
    // 将新消息加入队列头部
    mp->m_next = workq;
    workq = mp;
    // 解锁以允许其他线程操作队列
    pthread_mutex_unlock(&qlock);

    // 此时另一个线程可能在signal之前拿走了mp元素
    // 通过signal操作唤醒等待在qread上的线程
    pthread_cond_signal(&qread);
    // 如果在signal之前mp元素被拿走,workq仍然是NULL,需要继续等待
}

阻塞队列代码分析

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

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

自定义队列

当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。

阻塞队列类中,有些代码比较简单,这里仅对push和pop成员进行详解。

/*************************************************************
*循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;  
*线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
**************************************************************/
#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include "../lock/locker.h"
#include <stdlib.h>
#include <sys/time.h>

template<class T>
class block_queue {
public:
    /**
     * 构造函数,初始化队列
     * @param max_size 队列的最大容量,默认为1000
     */
    block_queue(int max_size = 1000) {
        // 检查最大容量是否合法
        if (max_size <= 0) {
            exit(-1);
        }
        m_max_size = max_size;
        // 分配存储空间
        m_array = new T[max_size];
        // 初始化队列大小为0
        m_size = 0;
        // 初始化队列前后指针
        m_front = -1;
        m_back = -1;
    }

// 析构函数,负责释放队列所占用的资源
~block_queue() {
    // 加锁以确保线程安全
    m_mutex.lock();
    // 检查数组是否被分配,如果被分配则释放内存
    if (m_array != nullptr) {
        delete[] m_array;
    }
    // 解锁以释放互斥锁
    m_mutex.unlock();
}

// 清空队列,重置队列状态
void clear() {
    // 加锁以确保线程安全
    m_mutex.lock();
    // 重置队列大小、前后指针,表示队列为空
    m_size = 0;
    m_front = -1;
    m_back = -1;
    // 解锁以释放互斥锁
    m_mutex.unlock();
}

// 检查队列是否已满
bool full() {
    // 加锁以确保线程安全
    m_mutex.lock();
    // 如果队列大小等于最大容量,则队列已满
    if (m_size >= m_max_size) {
        // 解锁并返回队列已满的状态
        m_mutex.unlock();
        return true;
    }
    // 解锁并返回队列未满的状态
    m_mutex.unlock();
    return false;
}

// 检查队列是否为空
bool empty() {
    // 加锁以确保线程安全
    m_mutex.lock();
    // 如果队列大小为0,则队列为空
    if(m_size == 0) {
        // 解锁并返回队列为空的状态
        m_mutex.unlock();
        return true;
    }
    // 解锁并返回队列不为空的状态
    m_mutex.unlock();
    return false;
}

// 获取队列的前端元素
bool front(T &value) {
    // 加锁以确保线程安全
    m_mutex.lock();
    // 如果队列为空,则不能获取元素
    if (0 == m_size) {
        m_mutex.unlock();
        return false;
    }
    // 将前端元素的值复制到传入的引用
    value = m_array[m_front];
    // 解锁以释放互斥锁
    m_mutex.unlock();
    return true;
}

/**
 * 获取队列的尾部元素值,并解锁
 * 
 * 该函数尝试从队列中获取尾部元素的值。在获取元素值前后,会加锁和解锁以确保线程安全。
 * 如果队列为空,则返回false,并且不会修改传入的引用参数。
 * 
 * @param value 将被设置为队列尾部元素值的引用,如果队列不为空。
 * @return 如果成功获取队列尾部元素值,则返回true;如果队列为空,则返回false。
 */
bool back(T& value) {
    m_mutex.lock(); // 加锁以保护对共享资源的访问
    if (0 == m_size) { // 检查队列是否为空
        m_mutex.unlock(); // 如果队列为空,解锁并返回false
        return false;
    }
    value = m_array[back]; // 将队列尾部元素值赋给value
    m_mutex.unlock(); // 解锁以释放对共享资源的访问
    return true; // 返回true表示成功获取队列尾部元素值
}

/**
 * 获取队列当前的元素数量,并解锁
 * 
 * 该函数通过加锁保护对共享资源的访问,以获取当前队列中的元素数量,
 * 然后解锁并返回这个数量。这用于获取队列的大小,而不改变队列的状态。
 * 
 * @return 队列当前的元素数量。
 */
int size() {
    int tmp = 0; // 用于存储队列当前的元素数量

    m_mutex.lock(); // 加锁以保护对共享资源的访问
    tmp = m_size; // 将队列当前的元素数量赋给tmp
    m_mutex.unlock(); // 解锁以释放对共享资源的访问
    return tmp; // 返回队列当前的元素数量
}

/**
 * 获取队列最大的元素数量,并解锁
 * 
 * 该函数通过加锁保护对共享资源的访问,以获取队列被创建时所允许的最大元素数量,
 * 然后解锁并返回这个最大数量。这用于确定队列的最大容量,而不改变队列的状态。
 * 
 * @return 队列最大的元素数量。
 */
int max_size() {
    int tmp = 0; // 用于存储队列最大的元素数量

    m_mutex.lock(); // 加锁以保护对共享资源的访问
    tmp = m_max_size; // 将队列最大的元素数量赋给tmp
    m_mutex.unlock(); // 解锁以释放对共享资源的访问
    return tmp; // 返回队列最大的元素数量
}

/**
 * 向队列中推送一个新元素
 * 
 * @param item 要推送的元素
 * @return bool 推送成功返回true,失败返回false
 */
bool push(const T &item) {
    // 加锁以确保线程安全
    m_mutex.lock(); 
    // 如果队列已满,则解锁并返回false
    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;
    
}

/**
 * 从队列中弹出一个元素
 * 
 * @param item 弹出的元素将存储在此处
 * @param ms_timeout 等待队列非空的超时时间(以毫秒为单位)
 * @return bool 弹出成功返回true,失败返回false
 */
bool pop(T &item, int ms_timeout) {
    // 初始化绝对超时时间
    struct timespec t = {0, 0};
    // 获取当前时间
    struct timeval now = {0, 0};
    gettimeofday(&now, nullptr);
    // 加锁以确保线程安全
    m_mutex.lock();
    
    // 如果队列为空且在超时时间内未变为非空,则解锁并返回false
    if (m_size <= 0) {
        // 设置超时的绝对时间
        t.tv_sec = now.tv_sec + ms_timeout / 1000;
        t.tv_nsec = (ms_timeout % 1000) * 1000;
        // 在超时时间内等待队列变为非空
        if (!m_cond.timewait(m_mutex.get(), t)) {
            m_mutex.unlock();
            return false;
        }
    }

    // 如果队列仍然为空,则解锁并返回false
    if(m_size <= 0) {
        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;

}

private:
    // 互斥锁,用于线程安全
    locker m_mutex;
    // 条件变量,用于线程间同步
    cond m_cond;

    // 队列存储数组
    T* m_array;
    // 当前队列大小
    int m_size;
    // 队列最大容量
    int m_max_size;
    // 队列首元素索引
    int m_front;
    // 队列末元素索引
    int m_back;
};
#endif

日志系统分为两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

本篇将介绍日志类的定义与使用,具体的涉及到基础API,流程图与日志类定义,功能实现。

基础API,描述fputs,可变参数宏__VA_ARGS__,fflush

流程图与日志类定义,描述日志系统整体运行流程,介绍日志类的具体定义

功能实现,结合代码分析同步、异步写文件逻辑,分析超行、按天分文件和日志分级的具体实现

基础API

为了更好的源码阅读体验,这里对一些API用法进行介绍。

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

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

//最简单的定义
#define my_print1(...)  printf(__VA_ARGS__)

//搭配va_list的format使用
#define my_print2(format, ...) printf(format, __VA_ARGS__)  
#define my_print3(format, ...) printf(format, ##__VA_ARGS__)

__VA_ARGS__宏前面加上##的作用在于,当可变参数的个数为0时,这里printf参数列表中的的##会把前面多余的","去掉,否则会编译出错,建议使用后面这种,使得程序更加健壮。

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

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

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

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

流程图与日志类定义

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

日志类定义

通过局部变量的懒汉单例模式创建日志实例,对其进行初始化生成日志文件后,格式化输出内容,并根据不同的写入方式,完成对应逻辑,写入日志文件。

日志类包括但不限于如下方法,

  • 公有的实例获取方法
  • 初始化日志文件方法
  • 异步日志写入方法,内部调用私有异步方法
  • 内容格式化方法
  • 刷新缓冲区

#ifndef LOG_H
#define LOG_H

#include <iostream>
#include <string>
#include "block_queue.h"

using namespace std;

/**
 * 日志类,用于记录程序运行时的日志信息
 */
class Log{
public:
    /**
     * 获取日志类的单例实例
     * @return 返回日志类的单例指针
     */
    static Log* get_instance() {
        static Log instance;
        return &instance;
    }

    /**
     * 日志刷新线程入口函数
     * @param args 线程参数,未使用
     * @return 返回nullptr
     */
    static void* flush_log_thread(void* args) {
        Log::get_instance()->async_write_log();
        return nullptr;
    }

    /**
     * 初始化日志类
     * @param file_name 日志文件名
     * @param close_log 是否关闭日志
     * @param log_buf_size 日志缓冲区大小,默认为8192
     * @param split_lines 日志文件分割行数,默认为5000000
     * @param max_queue_size 日志队列最大大小,默认为0表示不限制
     * @return 返回初始化是否成功
     */
    bool init(const char* file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);

    /**
     * 写入日志
     * @param level 日志级别
     * @param format 日志信息的格式字符串
     * @param ... 格式字符串中的参数
     */
    void write_log(int level, const char* format, ...);

    /**
     * 刷新日志,将缓冲区内容写入文件
     */
    void flush(void);

private:
    Log();
    virtual ~Log();
    
    /**
     * 异步写入日志线程函数
     * @return 返回nullptr
     */
    void* async_write_log() {
        string single_log;
        //从阻塞队列中取出一个日志string,写入文件
        while(m_log_queue->pop(single_log)) {
            m_mutex.lock();
            fputs(single_log.c_str(), m_fp);
            m_mutex.unlock();
        }
    }

private:
    char dir_name[128]; //日志目录名
    char log_name[128]; //日志文件名
    int m_split_lines; //日志文件分割行数
    int m_log_buf_size; //日志缓冲区大小
    long long m_count; //日志计数
    int m_today; //当日志
    FILE* m_fp; //文件指针
    char* m_buf; //缓冲区
    block_queue<string> *m_log_queue; //阻塞队列,用于异步写日志
    bool m_is_async; //是否异步写日志
    locker m_mutex; //线程锁
    int m_close_log; //是否关闭日志
};

// 定义DEBUG级别日志宏,当m_close_log为0时,记录DEBUG级别日志
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义INFO级别日志宏,当m_close_log为0时,记录INFO级别日志
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义WARN级别日志宏,当m_close_log为0时,记录WARN级别日志
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义ERROR级别日志宏,当m_close_log为0时,记录ERROR级别日志
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

#endif

日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏提供了其他程序的调用方法。

前述方法对日志等级进行分类,包括DEBUG,INFO,WARN和ERROR四种级别的日志。

功能实现

init函数实现日志创建、写入方式的判断。

write_log函数完成写入日志文件中的具体内容,主要实现日志分级、分文件、格式化输出内容。

生成日志文件 && 判断写入方式

通过单例模式获取唯一的日志类,调用init方法,初始化生成日志文件,服务器启动按当前时刻创建日志,前缀为时间,后缀为自定义log文件名,并记录创建日志的时间day和行数count。

写入方式通过初始化时是否设置队列大小(表示在队列中可以放几条数据)来判断,若队列大小为0,则为同步,否则为异步。

//异步需要设置阻塞队列长度,同步不需要设置
bool Log::init(const char* file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size) {
    // 如果设置了 max_queue_size,则将日志设置为异步模式
    if (max_queue_size >= 1) {
        m_is_async = true; // 设置异步标志为 true
        m_log_queue = new block_queue<string>(max_queue_size); // 创建日志队列,大小为 max_queue_size
        pthread_t tid;
        // 创建一个异步线程,用于异步写日志,flush_log_thread 是回调函数
        pthread_create(&tid, nullptr, flush_log_thread, nullptr);
    }

    m_close_log = close_log; // 设置日志关闭标志
    m_log_buf_size = log_buf_size; // 设置日志缓冲区大小
    m_buf = new char[m_log_buf_size]; // 分配日志缓冲区
    memset(m_buf, '\0', m_log_buf_size); // 将缓冲区初始化为全零
    m_split_lines = split_lines; // 设置日志分割行数

    time_t t = time(nullptr); // 获取当前时间
    struct tm* sys_tm = localtime(&t); // 将时间转换为本地时间
    struct tm my_tm = *sys_tm; // 复制本地时间结构体

    const char* p = strrchr(file_name, '/'); // 查找文件名中最后一个 '/' 的位置
    char log_full_name[256] = {0}; // 日志文件的完整路径和文件名

    if (p == nullptr) {
        // 如果文件名中没有 '/',直接使用文件名创建日志文件名
        snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
    } else {
        // 如果文件名中包含 '/',将目录和文件名分开
        strcpy(log_name, p + 1); // 提取文件名
        strncpy(dir_name, file_name, p - file_name + 1); // 提取目录名
        // 创建包含目录和日期的完整日志文件名
        snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
    }
    m_today = my_tm.tm_mday; // 记录日志创建的当天日期

    m_fp = fopen(log_full_name, "a"); // 以追加模式打开日志文件
    if (m_fp == nullptr) {
        return false; // 如果文件打开失败,返回 false
    }
    return true; // 文件打开成功,返回 true
}
日志分级与分文件

日志分级的实现大同小异,一般的会提供五种级别,具体的,

  • Debug,调试代码时的输出,在系统实际运行时,一般不使用。
  • Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
  • Info,报告系统当前的状态,当前执行的流程或接收的信息等。
  • Error和Fatal,输出系统的错误信息。

上述的使用方法仅仅是个人理解,在开发中具体如何选择等级因人而异。项目中给出了除Fatal外的四种分级,实际使用了Debug,Info和Error三种。

超行、按天分文件逻辑,具体的,

  • 日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
    • 若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
    • 若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log

将系统信息格式化后输出,具体为:格式化时间 + 格式化内容

void Log::write_log(int level, const char* format, ...) {
    struct timeval now = {0, 0};  // 定义一个时间结构体,用于存储当前时间
    gettimeofday(&now, nullptr);  // 获取当前时间,精确到微秒
    time_t t = now.tv_sec;  // 提取当前时间的秒部分
    struct tm *sys_tm = localtime(&t);  // 将秒部分转换为本地时间的tm结构
    struct tm my_tm = *sys_tm;  // 将sys_tm赋值给my_tm
    char s[16] = {0};  // 定义一个字符数组s,用于存储日志级别字符串
    switch (level) {
        case 0:
            strcpy(s, "[debug]");  // 如果level为0,表示debug级别日志
            break;
        case 1:
            strcpy(s, "[info]:");  // 如果level为1,表示info级别日志
            break;
        case 2:
            strcpy(s, "[warn]:");  // 如果level为2,表示warn级别日志
            break;
        case 3:
            strcpy(s, "[erro]:");  // 如果level为3,表示error级别日志
            break;
        default:
            strcpy(s, "[info]:");  // 其他情况下,默认使用info级别日志
            break;
    }
    
    m_mutex.lock();  // 加锁,确保多线程环境下对共享资源的安全访问
    m_count++;  // 日志计数器加1

    // 检查是否需要新建日志文件
    if(m_today != my_tm.tm_mday || m_count & m_split_lines == 0) {
        char new_log[256] = {0};  // 定义一个字符数组,用于存储新日志文件名
        fflush(m_fp);  // 刷新文件缓冲区,将数据写入文件
        fclose(m_fp);  // 关闭当前日志文件
        char tail[16] = {0};  // 定义一个字符数组,用于存储日期信息

        // 格式化日期信息
        snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
        if (m_today != my_tm.tm_mday) {
            // 如果是新的一天,则新建一个新的日志文件
            snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
            m_today = my_tm.tm_mday;  // 更新当前日期
            m_count = 0;  // 重置日志计数器
        } else {
            // 如果是同一天,但日志数量达到了分割行数,则创建一个新文件
            snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
        }
        m_fp = fopen(new_log, "a");  // 打开新日志文件,追加模式
    }

    m_mutex.unlock();  // 解锁

    va_list valst;  // 定义一个可变参数列表
    va_start(valst, format);  // 初始化可变参数列表

    string log_str;  // 定义一个字符串用于存储最终的日志信息
    m_mutex.lock();  // 再次加锁,确保安全写入日志内容

    // 写入具体的时间和日志级别内容
    int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
                     my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
                     my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
    
    // 将可变参数列表格式化并写入缓冲区
    int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);
    m_buf[n + m] = '\n';  // 添加换行符
    m_buf[n + m + 1] = '\0';  // 以空字符结束字符串
    log_str = m_buf;  // 将缓冲区内容转换为字符串

    m_mutex.unlock();  // 解锁

    if (m_is_async && !m_log_queue->full()) {
        // 如果是异步模式且日志队列未满,则将日志推入队列
        m_log_queue->push(log_str);
    } else {
        // 如果是同步模式或日志队列已满,则直接写入日志文件
        m_mutex.lock();
        fputs(log_str.c_str(), m_fp);  // 将日志字符串写入文件
        m_mutex.unlock();
    }

    va_end(valst);  // 结束可变参数列表

}

void Log::flush(void) {
    m_mutex.lock();  // 加锁
    fflush(m_fp);  // 刷新文件缓冲区,将数据写入文件
    m_mutex.unlock();  // 解锁
}
  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值