5.1 单例模式
引用定义:
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
从定义中可以看出,单例模式有以下几个重点:
- 这个类只能有一个实例
- 它必须自己创建这个实例
- 它必须自己向整个系统提供这个实例
懒汉模式和饿汉模式
懒汉模式:懒加载模式
-
优点:节省资源,在需要的时候创建对象。
-
缺点:线程不安全。获取对象的时候,效率低
-
最简单的线程安全的方式:同步方法,效率低
-
更好的的线程安全的方式:双重校验锁
源码:
//懒汉模式 + 锁 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 生产者消费者模型
下面代码是来自《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。
直接来看一下pop和push的源码:
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 日志写原理及实现
项目实现了两种写日志的方法:同步、异步。其中,异步显然是更好的选择:
- 所谓同步日志,即当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句。
- 使用异步日志进行输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,处理业务逻辑的主线程不用等待即可执行后续业务逻辑。
流程图
(图片出处:@两猿社)
- 日志文件
-
- 局部变量的懒汉模式获取实例
- 生成日志文件,并判断同步和异步写入方式
- 同步
-
- 判断是否分文件
- 直接格式化输出内容,将信息写入日志文件
- 异步
-
- 判断是否分文件
- 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件
基础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); 强制马上输出到控制台,可以避免出现上述错误。