目录
一、生产之消费者模型
1、什么是生产者消费者模型?
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
总结:“321”模型
- 3种关系:生产者和生产者之间是互斥关系(只有一个生产者可以通过临界区向临界资源写入);生产者和消费(同步);消费者和消费者(互斥)。
- 2种角色:生产者和消费者
- 1个交易场所:缓冲区(仓库)
2、生产者消费者模型的优点
生产者消费者模型,对具有很强耦合性的问题进行了解耦,同时支持高并发和“忙闲不均”。
3、基于Blocking Queue的生产者消费者模型
在多线程变成中阻塞队列(Blocking Queue)是一种常用于实现生产者消费者模型的数据结构。其与普通队列的区别在于,当队列为空时会阻塞从队列中取的操作,直到队列不为空;当队列为满时,会阻塞存放元素的操作,直到有元素被从队列中取出。
C++实现基于Blocking Queue的生产者消费者模型的实现
#include"ProducerConsumerModel.hpp"
void* ProducerRun(void* bq)
{
int count = 1;
while(true)
{
//每隔1秒生产一个
((BlockingQueue*)(bq))->PutMessage(count);
std::cout<<"生产者:生产成功-->"<<count++<<std::endl;
sleep(1);
}
}
void* ConsumerRun(void* bq)
{
sleep(1);
while(true)
{
//每隔2秒消费一个
int mesg;
((BlockingQueue*)(bq))->GetMessage(mesg);
std::cout<<"消费者:消费成功-->"<<mesg<<std::endl;
}
}
int main()
{
BlockingQueue *bq = new BlockingQueue;
//创建生产者和消费者线程:单生产者<--->单消费者
pthread_t p,c;//生产者消费者线程id:生产者->p 消费者->c
pthread_create(&p,NULL,ProducerRun,(void*)bq);
pthread_create(&c,NULL,ConsumerRun,(void*)bq);
//线程等待
pthread_join(p,NULL);
pthread_join(c,NULL);
return 0;
}
#ifndef __QUEUE_BLOCK_H_
#define __QUEUE_BLOCK_H_
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
class BlockingQueue
{
private:
//empty方法---判断阻塞队列是否为空
bool IsEmpty()
{
//如果size为0,为空表示假
return !bq.size();
}
bool IsFull()
{
//当阻塞队列中的元素个数等于阻塞队列容量时,队列为满
return bq.size() == capacity;
}
//full方法---判断阻塞队列是否为满
public:
//构造函数---创建一个大小为NUM的阻塞队列
BlockingQueue(size_t NUM = 8)
:capacity(NUM)
{
//初始化条件变量和互斥量
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&p_cond,NULL);
pthread_cond_init(&c_cond,NULL);
}
//put方法---向阻塞队列中放数据
void PutMessage(int mesg)
{
//当队列为空时,消费者向生产者发送一个信号,生产者开始放数据
pthread_mutex_lock(&mutex);
while(IsFull())
{
pthread_cond_signal(&c_cond);
std::cout<<"生产者:消费者,你可以请尽快消费..."<<std::endl;
std::cout<<"生产者:等待中..."<<std::endl;
pthread_cond_wait(&p_cond,&mutex);
}
bq.push(mesg);
pthread_mutex_unlock(&mutex);
}
//get方法---从阻塞队列中获取数据
void GetMessage(int& mesg)
{
//通过参数获取
pthread_mutex_lock(&mutex);
while(IsEmpty())
{
pthread_cond_signal(&p_cond);
std::cout<<"消费者:生产者,请尽快生产..."<<std::endl;
std::cout<<"消费者:等待中..."<<std::endl;
pthread_cond_wait(&c_cond,&mutex);
}
mesg = bq.front();
bq.pop();
pthread_mutex_unlock(&mutex);
}
//析构函数---销毁条件变量和互斥量
~BlockingQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&p_cond);
pthread_cond_destroy(&c_cond);
}
private:
std::queue<int> bq;//阻塞队列
size_t capacity;//阻塞队列的大小
pthread_cond_t p_cond;//生产者等待的条件变量
pthread_cond_t c_cond;//消费者等待的条件变量
pthread_mutex_t mutex;//互斥量
};
#endif
运行结果:
二、信号量
1、什么是信号量?
POSIX和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但是POSIX可以用于线程间资源的同步。
信号量的本质就是一个“计数器”!!!用来统计有效资源的数量,有两个操作(都是原子的):
- P操作:有效资源减少
- V操作:有效资源增加
2、信号量的作用
思考:是否可以将临界区代码分成互不影响的块,一个线程(执行流)访问一个区域?
答案:可以,使用信号量对临界资源进行管理,使用P、V操作访问释放临界资源。多个线程并发运行,提高运行效率。
3、信号量的使用
1)初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
2)销毁
int sem_destroy(sem_t* sem);
3)等待
功能:等待信号量,会将信号量的值减1 p操作
int sem_wait(sem_t *sem);
4)发布
功能:发布信号量,表示资源使用完毕归还资源,信号量值加1 V操作
int sem_post(sem_t *sem);
4、基于环形队列的生产者消费者模型
1)环形队列
- 环形队列采用数组模拟实现,利用模运算模拟换装特性
- 环形结构的起始状态和结束状态都是一样的(head==tail),不好判断未满或者为空,所以可以通过加标记为或者计数器判断满或者空,也可以通过预留一个空的位置作为满的状态
2)基于环形队列和信号量的生产者消费者模型的实现
#pragma once
#include<iostream>
#include<vector>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
class RingQueue
{
private:
void P(sem_t& sem)
{
//等待信号量,信号量的值-1
sem_wait(&sem);
}
void V(sem_t& sem)
{
//发布信号量,信号量的值+1
sem_post(&sem);
}
public:
RingQueue(size_t cap = 10)
:capacity(cap)
,p_index(0)
,c_index(0)
,rq(cap)
{
//消费者的信号量初始值为0
sem_init(&c_sem,0,0);
//生产者的信号量初始值为capacity
sem_init(&p_sem,0,capacity);
}
void PutMessage(int message)
{
//生产者放消息
P(p_sem);
rq[p_index++] = message;
p_index %= capacity;
V(c_sem);
}
void GetMessage(int& message)
{
P(c_sem);
message = rq[c_index++];
c_index %= capacity;
V(p_sem);
}
~RingQueue()
{
sem_destroy(&c_sem);
sem_destroy(&p_sem);
}
private:
std::vector<int> rq;//用数组模拟环形队列
size_t p_index;
size_t c_index;
sem_t p_sem;//生产者信号量
sem_t c_sem;//消费者信号量
size_t capacity;//环形队列的容量
};
#include"RingQueue.hpp"
void* consumer(void* arg)
{
RingQueue *rq = (RingQueue*)arg;
int message;
while(true)
{
rq->GetMessage(message);
std::cout<<"comsumer : get a message-> "<<message<<std::endl;
sleep(1);
}
return nullptr;
}
void* producer(void* arg)
{
RingQueue *rq = (RingQueue*)arg;
int count = 1;
while(true)
{
rq->PutMessage(count++);
std::cout<<"producer : put a message->"<<count<<std::endl;
count %= 11;
}
return nullptr;
}
int main()
{
RingQueue* rq = new RingQueue;
pthread_t c,p;
pthread_create(&c,nullptr,consumer,rq);
pthread_create(&p,nullptr,producer,rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
三、线程池
1、什么是线程池?
线程池是一种线程的使用模式。系统、服务器中的线程过多会带来调度开销,进而影响性能。而线程池中维护着多个线程,等待着监督管理者分配可并发执行的任务。避免了处理短时间任务时创建和销毁线程的代价。
2、线程池的优点
- 线程池保证了内核资源的充分利用(多个线程在内核中并发执行)
- 可以防止过度调度(线程池中线程数量是固定的,一般和内核有关,一般不会造成过度调度问题)
- 提高性能(提前创建线程,这些线程一直在运行,节省了处理任务的创建和销毁线程的开销)
- 相比进程池,线程池占用的资源更少,但是鲁棒性不强。
3、线程池的实现
#pragma once
#include<iostream>
#include<queue>
#include<unistd.h>
#include<pthread.h>
class Task
{
private:
int data;
public:
Task(){}
Task(int _data)
:data(_data)
{}
void Run()
{
std::cout<<"thread ["<<pthread_self()<<"] "<<data<<" pow is : "<<data*data<<std::endl;
}
};
class ThreadPool
{
private:
std::queue<Task*> q;//任务队列
pthread_cond_t cond;//只有线程池需要在该条件下等,服务器应该一直在从网络中获取数据
pthread_mutex_t mutex;//互斥锁,访问任务队列时需要加锁
size_t MaxNum;//最大线程数量
public:
ThreadPool(size_t size = 5)
:MaxNum(size)
{}
static void* Routine(void* arg)
{
ThreadPool* p_this = (ThreadPool*)arg;
while(true)
{
//从任务队列中获取任务,没有任务就休眠
pthread_mutex_lock(&(p_this->mutex));
while((p_this->q).empty())
{
pthread_cond_wait(&(p_this->cond),&(p_this->mutex));
}
Task t;
p_this->GetTask(t);
pthread_mutex_unlock((&p_this->mutex));
//执行任务
t.Run();
}
}
void ThreadPoolInit()
{
//初始化互斥锁和条件变量
pthread_cond_init(&cond,nullptr);
pthread_mutex_init(&mutex,nullptr);
//创建线程,并对线程进行分离
pthread_t tid;
for(size_t i = 0;i < MaxNum;i++)
{
pthread_create(&tid,nullptr,Routine,this);
}
}
void PutTask(Task& task)
{
//服务器从网络中获取任务放入任务队列
pthread_mutex_lock(&mutex);
q.push(&task);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
}
void GetTask(Task& task)
{
//线程池中的线程执行任务
Task* t = q.front();
q.pop();
task = *t;
}
~ThreadPool()
{
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
};
#include"ThreadPool.hpp"
int main()
{
ThreadPool tp;
tp.ThreadPoolInit();
//服务器端获取数据
while(true)
{
int x = rand()%10 + 1;
Task t(x);
tp.PutTask(t);
sleep(1);
}
}
四、单例模式
1、什么是单例模式
某些类只应该具有一个对象就称为单例。在很多的服务器开发场景中,往往需要将许多数据加载到内存中,此时往往要用一个单例类来管理这些数据。
2、懒汉方式和饿汉方式实现单例模式
五、STL智能指针和线程安全
STL中的容器、只能指针都不是线程安全的。
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
六、其他常见的锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁