1. 生产者消费者模型
1.1 123原则
1是:1个线程安全的队列
- 队列特性,先进先出
- 线程安全,需要保证在同一时刻,队列当中的元素只有一个执行流去访问
- 互斥锁+条件变量
2是:两种关系的线程
- 生产线程
- 消费线程
3是:3种关系
- 生产者与生产者互斥
- 消费者与消费者互斥
- 生产者与消费者同步+互斥
1.2 优点
- 支持忙闲不均
- 生产者与消费者解耦开来
- 支持高并发
1.3 图示
1.4 实现
思路:
-
队列:可以借助STL当中的queue
-
线程安全:STL中的容器都是线程不安全的
- 互斥:使用互斥锁
- 同步:使用条件变量
-
两种角色的线程:
- 生产者线程:负责往队列当中插入数据
- 消费者线程:负责出队列消费数据
使用C++ queue模拟阻塞队列的生产消费模型代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
//生产者消费者模型
//1. 一个线程安全的队列
//2. 两个角色的线程 生产线程&消费线程
//3.3种关系
//线程安全的队列
class BlockQueue
{
public:
BlockQueue(int capacity)
{
capacity_ = capacity;
pthread_mutex_init(&lock_,NULL);
pthread_cond_init(&prod_cond_,NULL);
pthread_cond_init(&cons_cond_,NULL);
}
~BlockQueue()
{
//销毁
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&prod_cond_);
pthread_cond_destroy(&cons_cond_);
}
//push存放数据
void Push(int data)
{
//加锁
pthread_mutex_lock(&lock_);
//如果队列已满,则阻塞等待
while(que_.size()>=capacity_)
{
pthread_cond_wait(&prod_cond_,&lock_);
}
//插入数据
que_.push(data);
//通知消费线程
pthread_cond_signal(&cons_cond_);
//解锁
pthread_mutex_unlock(&lock_);
}
//取出数据
void Pop(int *data)
{
pthread_mutex_lock(&lock_);
//如果队列为空,则阻塞等待
while(que_.empty())
{
pthread_cond_wait(&cons_cond_,&lock_);
}
//出队
*data = que_.front();
que_.pop();
//通知生产线程
pthread_cond_signal(&prod_cond_);
//解锁
pthread_mutex_unlock(&lock_);
}
private:
std::queue<int> que_;
//容量
int capacity_;
//互斥锁
pthread_mutex_t lock_;
//条件变量
pthread_cond_t prod_cond_;
pthread_cond_t cons_cond_;
};
//定义线程数量
#define THREADCOUNT 1
//生产线程入口函数
void* prodStart(void* arg)
{
BlockQueue* bq = (BlockQueue*)arg;
int data = 0;
while(1)
{
bq->Push(data);
printf("i am thread %p,push data:%d\n",pthread_self(),data);
data++;
}
return NULL;
}
//消费线程入口函数
void* consStart(void* arg)
{
BlockQueue* bq = (BlockQueue*)arg;
int data = 0;
while(1)
{
bq->Pop(&data);
printf("i am thread %p,pop data:%d\n",pthread_self(),data);
}
return NULL;
}
int main()
{
BlockQueue *blockQueue = new BlockQueue(10);
//创建线程
pthread_t prod_tid[THREADCOUNT],cons_tid[THREADCOUNT];
for(int i = 0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&prod_tid[i],NULL,prodStart,(void*)blockQueue);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&cons_tid[i],NULL,consStart,(void*)blockQueue);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main线程阻塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(prod_tid[i],NULL);
pthread_join(cons_tid[i],NULL);
}
return 0;
}
//编译
g++ $^ -o $@ -g -l pthread
运行结果(部分):
2. POSIX信号量
2.1 概念
- POSIX信号量和System V 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,但POSIX可以用于线程同步。
本质:
-
计数器+PCB等待队列+一堆接口(等待接口+唤醒接口)
-
计数器:本质是对资源的计数
- 当执行流获取信号量成功之后,信号量当中的计数器会进行减1操作,当获取失败后,该执行流就会被放到PCB等待队列当中
- 当执行流释放信号量成功后,信号量当中的计数器会进行加1操作
2.2 接口
2.2.1 定义
-
sem_t:
-eg:sem_t sem;
2.2.2 初始化
函数:
- int sem_init(sem_t *sem, int pshared, unsigned int value)
函数说明:
-
sem:传入信号量的地址
-
pshared:表示当前信号量是使用在进程间还是线程间
当使用sem_init初始化信号量为进程间的时候,会在内核中创建一块共享内存,来保存信号量的数据结构,其中资源计数器,PCB等待队列都是在共享内存当中维护的,所以我们调用唤醒或者等待接口的时候,就通过操作共享内存实现了不同进程间的通信,进而实现不同进程之间的同步与互斥。- 0:表示线程之间
- 1:表示进程之间
-
value:实际资源数量,用于初始化信号量当中资源计数器的
2.2.3 阻塞等待
- 如果调用等待接口进行获取信号量,会对资源计数器进行减1操作
- 如果判断当前信号当中资源计数器的值大于0,则能够成功获取信号量,表示可以访问临界资源,sem_wait函数会返回。
- 如果资源计数器小于等于0,则调用该接口的执行流被阻塞,该执行流被放到PCB等待队列中。
2.2.3.1 阻塞方式的等待
- int sem_wait(sem_t *sem)
2.2.3.2 非阻塞方式的等待
- int sem_trywait(sem_t *sem)
2.2.3.3 带有超时时间的等待
- int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)
2.2.4 唤醒
函数:
- int sem_post(sem_t *sem)
作用:
- 发布信号量,表示资源使用完成了,需要归还资源或者生产者重新生产了一个资源,对信号量当中的资源计数器进行加1操作。唤醒PCB等待队列当中的PCB
2.2.5 销毁信号量
- int sem_destroy(sem_t *sem)
2.2.6 实现同步与互斥
互斥:
- 初始化信号量当中的计数器为1,表示说只有一个资源可以被使用
- 当执行流A想要访问临界资源时,首先获取信号量,由于计数器中的值为1,表示可以访问,计数器的值从1变成0,从而执行流A去访问临界资源
- 此时当执行流B想要访问临界资源的时候,获取信号量,但是计数器当中的值为0,表示不能够访问临界资源,执行流B的PCB就被放到了PCB等待队列当中,同时信号量当中的计数器的值减1(0 --> -1),-1表示当前还有1个执行流在等待访问临界资源
同步:
-
不要求信号量当中的计数器一定为1,也可以为其他正整数
-
当执行流想要访问临界资源的时候,首先获取信号量
- 如果信号量当中的计数器大于0,则表示能够访问临界资源,则该执行流不会阻塞,顺序执行临界区代码
- 如果信号量当中的计数器值小于等于0,则表示不能访问临界资源,则该执行流会被放到PCB等待队列中,同时计数器也会进行减1操作
- 注意:如果计数器的值为负数,表示当前还有计数器的绝对值个执行流在等待
-
当释放信号量的时候,会对信号量当中的计数器进行加1操作,是否唤醒PCB等待队列当中的执行流
- 计数器加1操作之后还为负数或者为0,则需要通知PCB等待队列当中的执行流
- 计数器加1操作之后为正数,则不需要通知PCB等待队列
2.3 实现
基于环形队列的生产消费模型:环形队列采用数组模拟
代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <iostream>
#include <semaphore.h>
/*
* posix信号量 基于环形队列的生产消费模型
* 本质 计数器+PCB等待队列+一堆接口 计数器:本质是对资源的计数
* 1. 对于数组下标,pos = (pos+1)%数组大小
* 2. 对读写数组实现线程安全的时候
* 互斥: sem_t lock; sem_init(&lock,0,1);
* 同步:
* 生产者信号量: sem_t prod; sem_init(&prod,0,数组大小);
* 由于一开始数组当中没有空间可以读,则计数器初始值为0
* 消费者信号量: sem_t cons; sem_init(&cons,0,0);
*/
//定义数组容量大小
#define CAPACITY 4
//线程安全的队列
class RingQueue
{
public:
RingQueue()
:vec_(CAPACITY)
{
capacity_ = CAPACITY;
//初始化信号量
sem_init(&lock_,0,1);//初始化资源数为1,即只有一个线程在同一时刻能够拥有该信号量
sem_init(&prod_,0,capacity_);// 信号量计数器的值和数组的空闲空间一样大
// 在初始化时,由于数组没有一个有效元素,所有初始化资源数为0,后边生产线程在唤醒消费线程的时候,会对消费者信号量当中的计数器进行加加操作,从而消费者线程可以获取到消费信号量,进而去访问数组
sem_init(&cons_,0,0);
//初始化下标位置
pos_read_ = 0;
pos_write_ = 0;
}
~RingQueue()
{
//销毁
sem_destroy(&lock_);
sem_destroy(&prod_);
sem_destroy(&cons_);
}
void Push(int data)
{
sem_wait(&prod_);
sem_wait(&lock_);
vec_[pos_write_] = data;
pos_write_ = (pos_write_+1)%capacity_;
sem_post(&lock_);
sem_post(&cons_);
}
void Pop(int* data)
{
sem_wait(&cons_);
sem_wait(&lock_);
*data = vec_[pos_read_];
pos_read_ = (pos_read_+1)%capacity_;
sem_post(&lock_);
sem_post(&prod_);
}
private:
//数组
std::vector<int> vec_;
//数组容量
int capacity_;
//信号量
sem_t lock_; // 锁信号量 互斥
//同步
sem_t prod_; // 生产者信号量
sem_t cons_; // 消费者信号量
//读写位置 下标
int pos_write_;
int pos_read_;
};
#define THREADCOUNT 2
//生产线程入口函数
void* prodStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
int data = 0;
while(1)
{
rq->Push(data);
printf("i am prod_thread %p,push data:%d\n",pthread_self(),data);
data++;
}
return NULL;
}
//消费线程入口函数
void* consStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
int data;
while(1)
{
rq->Pop(&data);
printf("i am cons_thread %p,pop data:%d\n",pthread_self(),data);
}
return NULL;
}
int main()
{
RingQueue* rq = new RingQueue();
//创建线程
pthread_t prod_tid[THREADCOUNT],cons_tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&prod_tid[i],NULL,prodStart,(void*)rq);
if(ret<0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&cons_tid[i],NULL,consStart,(void*)rq);
if(ret<0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(prod_tid[i],NULL);
pthread_join(cons_tid[i],NULL);
}
delete rq;
rq = NULL;
return 0;
}
//编译
g++ $^ -o $@ -g -l pthread
运行结果(部分):
3. 读者写者问题(读写锁)
3.1 背景
在编写多线程时,有一种情况十分常见,那就是大量读,少量写的场景。
- 读的本质,只是去访问变量的内容,但是并没有修改,最终只是访问变量内容,并没有修改,不会导致程序结果的二义性
- 对于都是读的线程,即使不加锁,也不会对程序结果造成二义性,基于这种情况,加互斥锁反而会降低程序运行效率。
- 因此读写锁可以专门处理这种场景,需要注意的是写独占,读共享,写锁优先级高。
机制:
- 一旦读写锁发现,有执行流想要以写模式打开读写锁,即使后面有想要以读模式打开读写锁的线程,该线程也会阻塞掉
- 保证想要以写模式打开读写锁的线程一定能够拿到读写锁。
读写锁行为图示:
读写锁的三种状态:
- 读模式下的加锁状态
- 写模式下的加锁状态
- 不加锁的状态
3.2 读写锁接口
3.2.1 初始化
函数:
- int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr)
函数说明:
- pthread_rwlock_t:读写锁的类型,一般情况下在使用的时候,都是传入变量的地址
- pthread_rwlockattr_t :读写锁的属性,一般传递NULL,采用默认属性
3.2.2 销毁
函数:
- int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
3.2.3 加锁
3.2.3.1 读加锁
函数:
- int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
阻塞类型的读加锁接口 - int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
非阻塞类型的读加锁接口
函数说明:
- 最大的用处:允许多个线程以读加锁的方式获取到读写锁
- 本质上:
- 读写锁当中维护了一个引用计数,每当线程以读方式获取了读写锁,该引用计数进行++
- 当释放以读加锁方式的读写锁的时候,会先对引用计数进行–,直到引用计数的值为0的时候,才真正释放了这把读写锁
3.2.3.2 写加锁
函数:
- int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
类似于互斥锁,程序当中只允许一个线程,以写方式打开读写锁。
3.2.4 解锁
函数:
- int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
函数说明:
- 不管是以读方式获取的读写锁,还是以写方式获取的读写锁,使用该解锁接口都可以进行解锁
- 注意:以读方式获取的读写锁,引用计数为0的时候才真正释放了读写锁。
3.2.5 设置读写优先
函数:
- int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref)
函数说明:
-
attr:读写锁属性
- 示例:
- pthread_rwlockattr_t attr:定义读写锁属性
- pthread_rwlockattr_init(&attr):初始化
-
pref:属性宏定义
- PTHREAD_RWLOCK_PREFER_READER_NP (默认设置):读者优先,可能会导致写者饥饿情况
- PTHREAD_RWLOCK_PREFER_WRITER_NP:写者优先,目前有BUG,导致表现行为和读者优先一致。
- PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:写者优先,但写者不能递归加锁
3.3 代码示例
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <cstring>
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <cstdio>
//读写锁
volatile int ticket = 1000;
//定义读写锁
pthread_rwlock_t rwlock;
//读
void* reader(void* arg)
{
char* id = (char*)arg;
while(1)
{
//加锁
pthread_rwlock_rdlock(&rwlock);
if(ticket<=0)
{
//解锁
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n",id,ticket);
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return NULL;
}
//写
void* writer(void* arg)
{
char* id = (char*)arg;
while(1)
{
pthread_rwlock_wrlock(&rwlock);
if(ticket<=0)
{
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n",id,--ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return NULL;
}
struct ThreadAttr
{
pthread_t tid;
std::string id;
};
std::string create_reader_id(size_t i)
{
//利用ostringstream进行string 拼接
std::ostringstream oss("thread reader ",std::ios_base::ate);
oss<<i;
return oss.str();
}
std::string create_writer_id(size_t i)
{
std::ostringstream oss("thread writer ",std::ios_base::ate);
oss << i;
return oss.str();
}
//创建读线程
void init_readers(std::vector<ThreadAttr>& vec)
{
for(int i = 0;i<vec.size();i++)
{
vec[i].id = create_reader_id(i);
int ret = pthread_create(&vec[i].tid,NULL,reader,(void*)vec[i].id.c_str());
if(ret < 0)
{
perror("pthread_create");
return;
}
}
}
// 创建写线程
void init_writers(std::vector<ThreadAttr>& vec)
{
for(int i = 0;i<vec.size();i++)
{
vec[i].id = create_writer_id(i);
int ret = pthread_create(&vec[i].tid,NULL,writer,(void*)vec[i].id.c_str());
if(ret < 0)
{
perror("pthread_create");
return;
}
}
}
//回收线程
void join_threads(std::vector<ThreadAttr>const& vec)
{
//vector的迭代器
for(std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin();it != vec.rend();it++)
{
pthread_t const& tid = it->tid;
pthread_join(tid,NULL);
}
}
//初始化读写锁
void init_rwlock()
{
#if 0
//设置写优先
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NP);
pthread_rwlock_init(&rwlock,&attr);
pthread_rwlockattr_destroy(&attr);
#else
//默认读优先,会造成写饥饿
pthread_rwlock_init(&rwlock,NULL);
#endif
}
int main()
{
//定义写线程数量
const std::size_t reader_nr = 2000;
//定义读线程数量
const std::size_t writer_nr = 2;
std::vector<ThreadAttr> readers(reader_nr);
std::vector<ThreadAttr> writers(writer_nr);
//初始化读写锁
init_rwlock();
//创建读线程
init_readers(readers);
//创建写线程
init_writers(writers);
//main阻塞等待回收线程
join_threads(writers);
join_threads(readers);
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
//编译
g++ $^ -o $@ -g -l pthread
运行结果(部分):
如图,只能看到写饥饿。
4. 线程池
4.1 概念&使用场景
线程池:
- 本质:一个线程安全的队列+一堆线程
- 一种线程使用模式。线程过多会造成调度开销,进而影响缓存局部性和整体性能。而线程池维护多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价
- 线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
4.2 线程池示例
线程池中的线程,都是同一种角色的线程,言外之意,指的是每一个线程都执行同样的入口函数。
那么如何让相同入口函数的线程,处理不同的请求:
- switch case:处理大量不同请求的时候,比较麻烦
- 向线程池抛入数据的时候,将处理该数据的函数一起抛入(函数地址),线程池当中的线程只需要调用传入的函数处理传入的数据即可
线程处理数据采用的方式:
- 当从队列中拿出来的数据和处理业务数据混合在一起,如果某一个线程处理数据时间特别漫长,即只有一个线程在处理数据,其他线程都在等待获取队列当中的元素
- 将从队列当中拿数据和处理业务数据解耦开来,即加锁的时候只需要保证拿数据的时候互斥,在处理业务数据的时候,多个线程可以并行的去运行。
考虑让线程池中的线程退出:
- 原则:一定是线程池当中的线程安全队列没有数据可以处理了,这会才可以退出线程
- 灰度升级+负载均衡
4.3 代码示例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <iostream>
#include <vector>
//重定义一个返回值为void,参数为int的函数指针类型
typedef void (*Handler)(int);
//数据和处理数据的方式类
class QueueData
{
public:
QueueData(int data,Handler handler)
{
data_ = data;
handler_ = handler;
}
~QueueData(){}
//处理数据
void Run()
{
handler_(data_);
}
private:
int data_;
Handler handler_;
};
//线程池
class ThreadPool
{
public:
ThreadPool(int capacity,int thread_count)
{
capacity_ = capacity;
thread_count_ = thread_count;
//初始化互斥锁
pthread_mutex_init(&lock_,NULL);
//初始化条件变量
pthread_cond_init(&cons_cond_,NULL);
flag = 0;
}
~ThreadPool() //销毁
{
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&cons_cond_);
}
//创建线程
int THread_init()
{
pthread_t tid;
for(int i=0;i<thread_count_;i++)
{
int ret = pthread_create(&tid,NULL,PoolStart,(void*)this);
if(ret < 0)
{
perror("pthrad_create");
return -1;
}
}
return 0;
}
//往队列中push数据
int Push(QueueData* qd)
{
//加锁
pthread_mutex_lock(&lock_);
if(flag)
{
//退出前解锁
pthread_mutex_unlock(&lock_);
return -1;
}
que_.push(qd);
//解锁
pthread_mutex_unlock(&lock_);
//通知消费者线程
pthread_cond_signal(&cons_cond_);
return 0;
}
private:
void Pop(QueueData** qd)
{
*qd = que_.front();
que_.pop();
}
//线程入口函数
static void* PoolStart(void* arg)
{
//线程分离
pthread_detach(pthread_self());
ThreadPool *tp = (ThreadPool*)arg;
while(1)
{
//加锁
pthread_mutex_lock(&tp->lock_);
while(tp->que_.empty())
{
//等待
// 判断是否退出线程
if(tp->flag)
{
tp->thread_count_--;
pthread_mutex_unlock(&tp->lock_);
pthread_exit(NULL);
}
//队列为空进入等待
pthread_cond_wait(&tp->cons_cond_,&tp->lock_);
}
//队列中有数据
QueueData* qd;
tp->Pop(&qd);
//解锁
pthread_mutex_unlock(&tp->lock_);
//处理数据
qd->Run();
delete qd;
}
}
public:
//线程退出
void ThreadExit()
{
//加锁
pthread_mutex_lock(&lock_);
flag = 1;
pthread_mutex_unlock(&lock_);
//通知全部处于等待的线程
pthread_cond_broadcast(&cons_cond_);
}
private:
//线程安全的队列
size_t capacity_;//队列大小
std::queue<QueueData*> que_;
//互斥锁
pthread_mutex_t lock_;
//条件变量
pthread_cond_t cons_cond_; // 都是消费线程,只需要消费线程的条件变量
//线程的数量个数
int thread_count_;
// 中断标志位 线程退出标志
int flag;
};
//处理数据的函数
void DealData(int data)
{
printf("data:%d\n",data);
}
int main()
{
//创建一个线程池,队列大小为4,线程数量为2
ThreadPool* tp = new ThreadPool(4,2);
if(!tp)
{
printf("create ThreadPool failed\n");
return -1;
}
//创建线程
int ret = tp->THread_init();
if(ret<0)
{
printf("create ThreadPool failed\n");
return -1;
}
for(int i = 0;i<100;i++)
{
//向队列中存放数据
QueueData* qd = new QueueData(i,DealData);
if(!qd)
{
continue;
}
tp->Push(qd);
}
sleep(10);//休眠10s后让线程池中的线程退出
tp->ThreadExit();
delete tp;
#if 0
while(1)
{
sleep(1);
}
#endif
return 0;
}
运行结果:
5. 线程安全的单例模式
5.1 设计模式
设计模式的优点:
- 代码复用程度高
- 程序比较可靠,并且容易理解
- 代码框架比较稳定
设计模式的分类:
- 创建型模式:单例模式
- 结构型模式:适配器模式
- 行为型模式:观察者模式
5.2 单例模式
特点:
- 全局提供唯一一个类的实例,具有全局变量的特点
使用场景:
- 内存池,数据池
基础要点:
- 全局只有一个实例 ===> static+禁止构造+禁止拷贝构造+禁止赋值拷贝
- 线程安全
- 调用者是通过类的函数来获取实例
5.2.1 饿汉模式-线程不安全
程序启动的时候就进行初始化,资源在程序初始化的时候就全部加载完毕了
- 优点:程序运行速度很快,流畅
- 缺点:程序初始化的时候耗时比较长
代码示例:
template<class T>
class Singleton
{
private:
static T instance;//禁止构造,拷贝构造,赋值拷贝
public:
static T* GetInstance()
{
return &instance;
}
};
5.2.2 懒汉模式
资源在使用的时候才进行实例化,单例类的对象在使用的时候才进行实例化
- 优点:程序初始化的时候比较快
- 缺点:运行的时候没有饿汉式流畅,线程安全
5.2.2.1 懒汉模式-单线程版:
代码示例:
template<class T>
class Singleton
{
private:
static T* instance;
public:
static T* GetInstance()
{
if(instance==NULL)
{
instance = new T();
}
return instance;
}
};
5.2.2.2 懒汉模式-多线程版-性能低:
代码示例:
template<class T>
class Singleton
{
private:
static T* instance;
static std::mutex lock;
public:
static T* GetInstance()
{
//加锁
lock.lock();
if(instance == NULL)
{
instance = new T();
}
//解锁
lock.unlock();
return instance;
}
};
5.2.2.3 懒汉模式-多线程版-性能高:
代码示例:
template<class T>
class Singleton
{
private:
volatile static T* instance; // volatile 禁止指令重排序
static std::mutex lock;
public:
static T* GetInstance()
{
if(instance==NULL)
{
//加锁
lock.lock();
if(instance==NULL) // 双重校验锁 ,降低锁冲突的概率,提高性能
//使用互斥锁,保证多线程情况下也只调用一次new
{
instance = new T();
}
//解锁
lock.unlock();
}
return instance;
}
};