09 日志系统(上)

09 日志系统(上)

基础知识

**日志,**由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。
同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
生产者-消费者模型,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。
阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。
单例模式,保证一个类只创建一个实例,同时提供全局访问的方法。

整体概述

使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。
异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。
日志系统大致可以分为两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

本文内容

介绍单例模式与阻塞队列的定义,涉及单例模式、生产者-消费者模型、阻塞队列的代码实现。
单例模式,描述懒汉与饿汉两种单例模式,并结合线程安全进行讨论。
生产者-消费者模型,描述条件变量,基于该同步机制实现简单的生产者-消费者模型。
代码实现,结合代码对阻塞队列的设计进行详解。

单例模式

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。
单例模式有两种实现方法,懒汉和饿汉。懒汉模式,不用的时候不去初始化,在第一次被使用时才进行初始化;饿汉模式,在程序运行时立即初始化。

经典的线程安全懒汉模式

双检测锁模式

class single{
private:
	//私有静态指针变量指向唯一实例
	static single *p;

	//静态锁,是由于静态函数只能访问静态成员
	static pthread_mutex_t lock;

	//私有化构造函数
	single(){
		pthread_mutex_init(&lock,NULL);
	}
	~single(){}

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

pthread_mutex_t single::lock;

single* single::p=NULL;
single* single::getinstance(){
	if(NULL==p){
		pthread_mutex_lock(&lock);
		if(NULL==p){
			p=new single;
		}
		pthread_mutex_unlock(&lock);
	}
	return p;
}

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

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

双检测锁模式,写起来不太优雅。
更优雅的单例模式实现,使用函数内的局部静态对象

class single{
private:
	single(){}
	~single(){}

public:
	static single* getinstance();
};

single* single::getinstance(){
	static single obj;
	return &obj;
}

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

class single{
private:
	static pthread_mutex_t lock;
	single(){
		pthread_mutex_init(&lock,NULL);
	}
	~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(),都只不过是返回一个对象的指针而已。是线程安全的,不需要在获取实例的成员函数中加锁。

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;

	system("pause");
	return 0;
}

饿汉模式存在隐藏的问题:非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用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,&metex);
}
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是可以的。

生产者-消费者模型

process_msg相当于消费者,enqueue_msg相当于生产者,struct msg* workq作为缓冲队列
生产者和消费者是互斥关系,两者对缓冲区访问互斥,同时生产者和消费者又是一个相互协作与同步的关系,只有生产者生产之后,消费者才能消费。

#include <pthread.h>
struct msg{
	struct msg *m_next;
};

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);
		}
		mq=workq;
		workq=mp->m_next;
		pthread_mutex_unlock(&qlock);
	}
}

void enqueue_msg(struct msg* mp){
	pthread_mutex_lock(&qlock);
	mp->m_next=workq;
	workq=mp;
	pthread_mutex_unlock(&qlock);
	pthread_cond_signal(&qready);
}

阻塞队列代码分析

当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。
阻塞队列类中,有些代码比较简单,这里仅对push和pop成员进行详解。

class block_queue
{
public:
	//初始化私有成员
	block_queue(int max_size=1000)
	{
		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;

		//创建互斥锁和条件变量
		m_mutex=new pthread_mutex_t;
		m_cond=new pthread_cond_t;
		pthread_mutex_init(m_mutex,NULL);
		pthread_cond_init(m_cond,NULL);
	}

	//往队列中添加元素,需要将所有使用队列的线程先唤醒
	//当有元素push进队列,相当于生产者生产了一个元素
	//若当前没有线程等待条件变量,则唤醒无意义
	bool push(const T &item)
	{
		pthread_mutex_lock(m_mutex);
		if(m_size>=m_max_size)
		{
			pthread_cond_broadcast(m_cond);
			pthread_mutex_unlock(m_mutex);
			return false;
		}

		//将新增数据放在循环数组的对应位置
		m_back=(m_back+1)%m_max_size;
		m_array[back]=item;
		m_size++;

		pthread_cond_broadcast(m_cond);
		pthread_mutex_unlock(m_mutex);

		return true;
	}

	//pop时,如果当前队列没有元素,将会等待条件变量
	bool pop(T &item)
	{
		pthread_mutex_lock(m_mutex);

		//多个消费者的时候,这里是用while而不是if
		while(m_size<=0)
		{
			//当重新抢到互斥锁,pthread_cond_wait返回为0
			if(0!=pthread_cond_wait(m_cond,m_mutex))
			{
				pthread_mutex_unlock(m_mutex);
				return false;
			}
		}

		//取出队列首的元素,使用循环数组模拟的队列
		m_fornt=(m_front+1)%m_max_size;
		item=m_array[m_front];
		m_size--;
		pthread_mutex_unlock(m_mutex);
		return true;
	}

	//增加了超时处理
	//在pthread_cond_wait基础上增加了等待的时间,只指定时间内能抢到互斥锁即可
	//其他逻辑不变
	bool pop(T &item,int ms_timeout)
	{
		struct timespec t={0,0};
		struct timeval now={0,0};
		gettimeofday(&now,NULL);
		
		pthread_mutex_lock(m_mutex);

		
		if(m_size<=0)
		{
			t.tv_sec=now.tv_sec+ms_timeout/1000;
			t.tv_nsec=(ms_timeout%1000)*1000;
			if(0!=pthread_cond_timewait(m_cond,m_mutex,&t))
			{
				pthread_mutex_unlock(m_mutex);
				return false;
			}
		}

		if(m_size<=0)
		{
			pthread_mutex_unlock(m_mutex);
			return false;
		}
		//取出队列首的元素,使用循环数组模拟的队列
		m_fornt=(m_front+1)%m_max_size;
		item=m_array[m_front];
		m_size--;
		pthread_mutex_unlock(m_mutex);
		return true;
	}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值