1.基本概念
日志,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件,所以用c++写服务器后端,日志需要我们来实现,关于具体怎么来实现,我们要进行设计。
同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降。意思就是工作线程要等到日志写入函数执行完毕以后才能工作,但是由于日志内容多,比较大,需要很长时间执行I/O操作,那么工作线程就会有很长时间不能工作。
生产者-消费者模型,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。
阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。其实都是利用了一个缓冲区。
单例模式,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法。
2.整体概述
日志系统大致可以分成两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。
单例模式,描述懒汉与饿汉两种单例模式,并结合线程安全进行讨论。
生产者-消费者模型,描述条件变量,基于该同步机制实现简单的生产者-消费者模型。这个本科的操作系统课程都学过。
3.单例模式
单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。
关于为什么使用static:
(1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
(2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
(3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
(5)保证数据安全性。
4.经典的线程安全懒汉模式
经典的线程安全懒汉模式,使用双检测锁模式。
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的情况,直接返回已创建好的实例。
5.局部静态变量之线程安全懒汉模式
这种方法不需要进行加锁和解锁
class single{
private:
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::getinstance(){
static single obj;
return &obj;
}
所以,如果使用C++11之前的标准,还是需要加锁,之后不需要加锁。
6.饿汉模式
饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。
跟懒汉有什么不同???
都是为了实现线程安全,上面的使用双检测锁机制,在c++11之后可以保证安全而不用使用,那么饿汉模式也不需要用锁的原因是不同的线程可以虽然可以调用函数实例化,但是返回的是对象的指针所以是安全的嘛。那懒汉 返回的不是对象的指针嘛 ?c++11之后是可以理解的,因为他是静态局部变量,每一次都在实例化函数中间定义,返回的应该是不用的指向对象的指针。
懒汉模式:即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化(实例的初始化放在getinstance函数内部)
经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次)
利用局部静态变量实现线程安全懒汉模式
饿汉模式:即迫不及待,在程序运行时立即初始化(实例的初始化放在getinstance函数外部,getinstance函数仅返回该唯一实例的指针)。
#include <bits/stdc++.h>
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;
system("pause");
return 0;
}
说明p1和p2调用实例化函数后指向的是同一个地方。
7.再次条件变量
条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。
基础API
pthread_cond_init函数,用于初始化条件变量
pthread_cond_destory函数,销毁条件变量
pthread_cond_broadcast函数,以广播的方式唤醒所有等待目标条件变量的线程
pthread_cond_wait函数,用于等待目标条件变量,使线程阻塞。该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,表示重新抢到了互斥锁,互斥锁会再次被锁上, 也就是说函数内部会有一次解锁和加锁操作。
8.阻塞队列
当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。
阻塞队列类中,有些代码比较简单,这里仅对push和pop成员进行详解。
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;
}
//pop时,如果当前队列没有元素,将会等待条件变量
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;
}