三、日志系统log:log.h\log.cpp\block_queue.h
前置知识
基础知识
-
日志:服务器自动创建,记录
运行状态
,错误信息
,访问数据的文件
。 -
同步日志:串行模式,日志写入线程和工作线程串行执行,即当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句。
由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
-
生产者-消费者模式:
-
生产者和消费者共享一个存储空间,只有缓冲区没满,生产者才能往缓冲区写入数据,否则必须等待。
-
只有缓冲区非空,消费者才能从缓冲区取出,否则必须等待。
-
缓冲区是临界资源,各进程必须互斥的访问。
-
-
阻塞队列:
- 将生产者消费者模式进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
-
异步日志:
- 生产者消费者模式就是一种异步日志。
- 将所写的内容先放入阻塞队列,写线程从阻塞队列中取出,写入日志。
-
单例模式:
- 设计模式之一,保证一个类只创建一个实例,看《大话设计模式》21章。
概述
-
使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。
-
其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。
-
日志系统大致可以分成两部分
- 其一是单例模式与阻塞队列的定义
- 其二是日志类的定义与使用。
概念解析
1.单例模式
最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
实现思路:
- 私有化构造函数,防止外界创建单例类对象;
- 使用类的私有静态指针指向该单例类的唯一实例;
- 通过调用类的公有静态函数来创建/获取单例类的对象;
两种模式:
- 懒汉模式:第一次使用时才进行初始化。
- 饿汉模式:程序开始时立即初始化。
经典的线程安全懒汉模式思路示例代码
使用
双检测锁模式
,也就是获得对象指针的时候,首先检测指针是否为空【NULL】,是的就上锁,第二次检测是不是空【NULL】,第二次检测是防止在第一次检测到加锁这段时间内有别的线程创建了对象
。之后若还是NULL,则创建对象,解锁,返回指针。非空就直接返回对象。好处是不用每次获得指针都要上锁解锁,只需要判断是否非空。class single { private: //私有静态指针,指向唯一实例 static shared_ptr<single> p; static pthread_mutex_t lock; //私有构造函数 single() { pthread_mutex_init(&lock, NULL); } //私有析构函数 //不在析构函数里边使用delete的原因有2: //1、当进程结束后,系统会自动回收进程所有资源,而单例模式一般都是在整个进程存活期间都存在的,进程结束>>就自动释放了,所以不需要delete多此一举。 //2、当使用delete的时候类会调用析构函数,这时候析构函数中又有delete,又调用了本身,会陷入递归调用析>>构函数的情况,所以也不能在析构函数中delete本身。 ~single(){} public: //公有静态方法获取实例 static single *getinstance(); }; //static对象要在类外初始化 pthread_mutex_t single::lock; shared_ptr<single> p = nullptr; single *single::getinstance() { if(p == nullptr) { pthread_mutex_lock(&lock); if(p == nullptr) { p = make_shared<single>(single); } pthread_mutex_unlock(&lock); } return p; }
使用局部静态变量的线程安全懒汉模式
C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁,其中C++0x是C++11标准成为正式标准之前的草案临时名字。
如果是使用C++11标准之前的标准,那就用下边带锁的方法,如果是C++11,那就把带!的去掉就是了。class single { private: pthread_mutex_t lock;//!! single(){ pthread_mutex_init(&lock, NULL);//!! } ~single(){} public: static single *getinstance(); }; single pthread_mutex_t lock ;//!! single *single::getinstance() { pthread_mutex_lock(&lock);//!! static single p; pthread_mutex_unlock(&lock);//!! return &p; }
饿汉模式:
饿汉模式在程序运行时就定义了对象,并初始化,所以不需要加锁,不管哪一个线程调用成员函数getinstance()都只是返回一个对象的指针。
class single { private: single(){} ~single(){} public: static single *p; static single *getinstance(); }; single *single::p = new single; single *single::getinstance() { return p; }
2.条件变量的一些注意点
在之前写条件变量的时候有一个例子,现在解析这个例子中的一些需要注意的点
while(1) { pthread_mutex_lock(&mutex); iCount++; pthread_mutex_unlock(&mutex); if (iCount >= 100) { pthread_cond_signal(&cond); } } //thread4: while (1) { //(1)先上锁。 pthread_mutex_lock(&mutex); //上锁 //(2)使用while而不是if while(iCount < 100)//不满足条件就等待 { //解锁,放入等待队列,其他线程可以继续使用这个锁,这样也不会因为反复询问消耗内核资源,等条件满足>了,再次上锁,然后返回。 pthread_cond_wait(&cond, &mutex); } //在这里执行一些操作 printf("iCount >= 100\r\n"); iCount = 0; /// pthread_mutex_unlock(&mutex); }
(1)要先上锁,一个是pthread_cond_wait是需要锁是锁上的状态,因为他会执行一个解锁操作;另一个是为了保证线>程安全,防止竞争访问共享资源。
(2)首先iCount是大家都能访问且修改的所谓的
"资源个数"
,假如使用if,pthread_cond_wait收到消息进行加>锁,然后执行下一步,那如果这时候这个资源被其他线程使用了,那这个时候直接跳出if,就只能访问不存在的资源。所>以要使用while,这样再进行一次判断,发现确实资源还在,才跳出while,进行下一步执行//如果使用if if(iCount < 100)//不满足条件就进入pthread_cond_wait等待条件满足,就退出if块 { //解锁,放入等待队列,其他线程可以继续使用这个锁,这样也不会因为反复询问消耗内核资源,等条件满足了,再>次上锁,然后返回。 pthread_cond_wait(&cond, &mutex); }
3.生产者消费者模型
《Unix 环境高级编程》【P334(355)】中第11章线程关于pthread_cond_wait的介绍中有一个生产者-消费者的例子
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(workq == NULL) { pthread_cond_wait(&qread, &qlock); } mp = workq; //把拿到的资源消耗掉,把workq指向下一位 workq = mp->m_next; pthread_mutex_unlock(&qlock); } } //生产者,生产资源workq 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 ,因此需要继续等待*/ }
源码解析
log文件夹下的block_queue.h
文件
#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H
#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
template<class T>
class block_queue
{
public:
//构造函数
block_queue(int max_size = 1000)
{
if(max_size <= 0)
{
printf("The max_num of block_queue must be lager than 0!");
exit(-1);
}
m_max_size = max_size;
//创建队列
m_array = new T[m_max_size];
//用"标记"标记当前个数和位置。
m_size = 0;
m_front = -1;
m_back = -1;
}
//析构函数,释放m_array申请的空间
~block_queue()
{
//防止重复删除
m_mutex.lock();
if (m_array != NULL)
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();
if(m_size == 0)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素,用引用返回,顺便判断返回是否成功
bool front(T &value)
{
m_mutex.lock();
if(m_size == 0)
{
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回尾元素
bool back(T &value)
{
m_mutex.lock();
if(m_size == 0)
{
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}
//返回元素个数,也就是m_size的个数
int size()
{
int tmp = 0;
m_mutex.lock();
//用临时变量tmp存储,因为不能直接返回,要解锁后返回,而一解锁m_size就有可能变化,所以存储临时量。
tmp = m_size;
m_mutex.unlock();
return tmp;
}
//返回最大值
int max_size()
{
int tmp = 0;
m_mutex.lock();
//个人觉得,如果m_max_size的量不会变,而且一开始就初始化了,max_size的使用一定是在构造函数之后的,也就没必要锁上了。
tmp = m_max_size;
m_mutex.unlock();
return tmp;
}
//往队列添加元素
bool push(const T &item)
{
m_mutex.lock();
//如果m_size的数量太多了,就没法添加了
if(m_size >= m_max_size)
{
//广播通知所有等待的线程,如果没有等待的,则唤醒无意义
m_cond.broadcast();
m_mutex.unlock();
//添加失败
return false;
}
/*
//个人觉得上边这段可以这么改
if(m_size >= m_max_size)
{
//广播通知所有等待的线程,如果没有等待的,则唤醒无意义
m_cond.broadcast();
m_mutex.unlock();
//增加一个二次判断,如果恰好有等待的线程用了,还能添加成功。(不过这样做会增加内核负担,是否合理不知道)
m_mutex.lock();
if(m_size >= m_max_size)
{
m_mutex.unlock();
return false;//添加失败
}
}
*/
m_back = (m_back + 1) % m_max_size;
m_array[m_bcak] = item;
++m_size;
m_cond.broadcast();
m_mutex.unlock();
return true;
}
//弹出首元素
bool pop(T &item)
{
m_mutex.lock();
//没有资源就一直等
while(m_size <= 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;
}
//增加超时处理
bool pop(T &item, int ms_timeout)
{
struct timespec t = {0, 0};
struct timeval now = {0, 0};
gettimeofday(&now, NULL);//获取当前时间
m_mutex.lock();
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))//在指定时间抢不到,或者超时,就退出返回false
{
m_mutex.unlock();
return false;
}
}
//再次判断,防止被抢,那为什么不用while?
if (m_size <= 0)
{
m_mutex.unlock();
return false;
}
/*//上边的两个if可否换成
while(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))//在指定时间抢不到,或者超时,就退出返回false
{
m_mutex.unlock();
return false;
}
struct timeval now2 = {0, 0};
gettimeofday(&now2, NULL);
//重新更新等待时间
ms_timeout = (t.tv_sec - now2.tv_sec) * 1000;
}
*/
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