目录
信号量
为什么需要信号量
引入信号量
POSIX信号量相关的接口函数
基于环形队列的生产消费模型
引入环形队列
信号量保护环形队列的原理
单生产单消费的环形队列的生产消费模型
多生产多消费的环形队列的生产消费模型
生产消费者模型高效在哪里?
申请信号量的本质:对临界资源中特定小块资源的预定机制
解决临界资源被多个执行流访问引发的安全问题:
我们之前的选择是通过互斥量mutex进行加锁,将临界资源整个保护起来,每次只让一个执行流访问该临界资源。
现在通过信号量,我们可以将临界资源划分为特定的小块资源,支持多个执行流同时访问该临界资源的不同区域的小块资源,此时也不会引发安全问题。
信号量的本质是一个计数器 ---- 用来衡量临界资源中资源数量多少
只要拥有信号量,就一定能够拥有临界资源的一部分。(类比电影院买票,买到票一定有自己的位置)
信号量本身也是临界资源
所以我们也需要保证其操作的原子性
信号量的PV操作:
P操作:我们将申请信号量称为P操作,本质就是申请获得临界资源中特定资源的使用权限,当申请成功时临界资源中该资源的数量需要减1,因此P操作的本质就是让计数器减1。
V操作:我们将释放信号量称为V操作,本质就是归还临界资源中特定资源的使用权限,当释放成功时临界资源中该资源的数目就需要加1,因此V操作的本质就是让计数器加1
信号量申请失败
当用来标示临界资源中特定资源的信号量为0时就代表这部分资源都已经用完了。此时再进行申请就会申请失败,当前执行流就会进入当前信号量的等待队列进行等待,当有该资源时再唤醒该执行流。
注意:信号量内部并非只有一个计数器,还有着相应的等待队列。
信号量的意义
在访问临界资源之前,我们就能通过信号量提前知道临界资源的使用情况。
POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
使用信号量需要包含下面的库
#include
接口介绍:
初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数介绍:
sem:需要初始化的信号量。
pshared:0表示线程间共享,非0表示进程间共享。
value:信号量的初始值(特定资源的初始数量)。
返回值:
信号量初始化成功返回0,失败返回-1。
发布信号量
int sem_post(sem_t *sem);
参数介绍:
需要进行P操作的信号量(发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。)
返回值:
发布信号量成功返回0,信号量的值加一。
发布信号量失败返回-1,信号量的值保持不变。
等待信号量
int sem_wait(sem_t *sem);
参数介绍:
需要进行V操作的信号量(等待信号量,会将信号量的值减1)
返回值:
等待信号量成功返回0,信号量的值减一。
等待信号量失败返回-1,信号量的值保持不变。
销毁信号量
int sem_destroy(sem_t *sem);
参数介绍:
待销毁的信号量
返回值:
销毁信号量成功返回0,失败则返回-1。
环形队列大家肯定不陌生了,通过数组模拟环形队列时我们有两种方法:
通过计数判断空和满。
空一个位置。
在接下来的模拟实现中就是用的第一种方法,用信号量来充当对应资源的计数,以此来判断空和满。
只有当生产者和消费者指向同一个位置并访问时,才可能会导致数据不一致的问题。
生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
环形队列为空时。
环形队列为满时。
但是在这两种情况下,生产者和消费者无法同时对环形队列进行访问:
当环形队列为空的时,消费者无法申请信号量,因为此时数据资源为0,只能先让生产者进行生产。
当环形队列为满的时,生产者无法申请信号量,因为此时空间资源为0,只能让消费者先消费。
为了方便讲解,先只模拟实现单生产单消费(只维持生产者和消费者之间的互斥同步关系)
//RingQueue.hpp
#pragma once
#include
#include
#include
template
class RingQueue
{
const static int cap = 8;
public:
RingQueue(const int cap = cap)
:queue(cap)
,ProducterStep(0)
,ConsumerStep(0)
{
sem_init(&semSpace,0,cap);
sem_init(&semProduct,0,0);
}
~RingQueue()
{
sem_destroy(&semSpace);
sem_destroy(&semProduct);
delete queue;
}
void Push(const T& data)
{
sem_wait(&semSpace); //如果已经满了,就阻塞
queue[ProducterStep++] = data;
ProducterStep%=cap;
sem_post(&semProduct); //生产完后,商品+1
}
void Pop(T* data)
{
sem_wait(&semProduct); //如果队列为空,就阻塞。
*data = queue[ConsumerStep++];
ConsumerStep%=cap;
sem_post(&semSpace); //消费完成后,空位+1
}
private:
std::vector queue;
int _cap; //循环队列容量
sem_t semSpace; //队列中空位的个数
sem_t semProduct; //队列中待消费数据的个数
int ConsumerStep; //消费者所在下标
int ProducterStep; //生产者所在下标
};
注意点:
我们用semSpace来代表环形队列中的剩余空间数量,用semProduct来代表环形队列中待消费数据的数量。
ConsumerStep代表消费者拿数据的下标,ProducterStep代表生产者放数据的下标。
等待信号量semSpace后,表示剩余空间数量减一,之后应该发布信号量,表示带消费数据的数量+1。(即生产者生产后,空位少了一个,资源多了一个) 反之亦然。
因为是单生产,单消费,所以暂时不维持生产者和生产者之间的互斥关系,消费者和消费者之间的互斥关系。
接下来尝试使用该队列:
#include "RingQueue.hpp"
#include
#include
#include
#include
void* Consumer(void* _rq) //消费者
{
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) //消费
{
int data;
rq->Pop(&data);
printf("消费者消费了数据------%d\n",data);
}
return nullptr;
}
void* Producter(void* _rq) //生产者
{
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) // 生产
{
int data = rand()%20 + 1;
rq->Push(data);
printf("生产者生产了数据 ---- %d\n",data);
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ pthread_self()); //生产随机数种子
RingQueue* rq = new RingQueue();
pthread_t c,p;
pthread_create(&c,nullptr,Consumer,rq);
pthread_create(&p,nullptr,Producter,rq);
//回收资源
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
先只传入整数来进行测试:
接下来再传入方法来测试一下:
#pragma once
#include
class Task
{
public:
Task(const int& x = 1, const int& y = 1, const char op = '+')
: a(x), b(y), _op(op)
{
}
~Task()
{
}
void Run()
{
int ret = 0;
switch (_op)
{
case ('+'):
ret = a+b;
break;
case ('-'):
ret = a-b;
break;
case ('*'):
ret = a*b;
break;
case ('/'):
ret = a/b;//暂不考虑b为0;
break;
case ('%'):
ret = a%b;//暂不考虑b为0
break;
default:
std::cout << "未知操作符" << std::endl;
break;
}
std::cout<< a << _op << b << '=' << ret << std::endl;
}
void operator()()
{
Run();
}
private:
int a;
int b;
char _op;
};
该方法主要是进行简单的运算,为了方便展示结果,重载了() 。
进行测试:
#include "Task.hpp"
#include "RingQueue.hpp"
#include
#include
#include
#include
void* Consumer(void* _rq)
{
//RingQueue* rq = reinterpret_cast*>(_rq)
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) //消费
{
Task t;
rq->Pop(&t);
std::cout<<"消费者处理任务:" ;
t();
}
return nullptr;
}
void* Producter(void* _rq)
{
//RingQueue* rq = reinterpret_cast*>(_rq);
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) // 生产
{
int x = rand()%10+1;
int y = rand()%20+1;
char op = "+-*/%"[rand()%5];
Task t1(x,y,op);
std::cout<<"生产了任务------"<< x << op << y << "=?"<Push(t1);
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ pthread_self()); //生产随机数种子
//RingQueue* rq = new RingQueue();
RingQueue* rq = new RingQueue();
pthread_t c,p;
pthread_create(&c,nullptr,Consumer,rq);
pthread_create(&p,nullptr,Producter,rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
实现多生产多消费只需要我们再加上两把锁分别保护消费和生产的过程就可以了。
#pragma once
#include
#include
#include
#include
template
class RingQueue
{
const static int cap = 8;
public:
RingQueue(const int cap = cap)
:queue(cap)
,ProducterStep(0)
,ConsumerStep(0)
{
sem_init(&semSpace,0,cap);
sem_init(&semProduct,0,0);
pthread_mutex_init(&pmutex,nullptr);
pthread_mutex_init(&cmutex,nullptr);
}
~RingQueue()
{
sem_destroy(&semSpace);
sem_destroy(&semProduct);
pthread_mutex_destroy(&pmutex);
pthread_mutex_destroy(&cmutex);
delete queue;
}
void Push(const T& data)
{
sem_wait(&semSpace);
pthread_mutex_lock(&pmutex);
queue[ProducterStep++] = data;
ProducterStep%=cap;
pthread_mutex_unlock(&pmutex);
sem_post(&semProduct);
}
void Pop(T* data)
{
sem_wait(&semProduct);
pthread_mutex_lock(&cmutex);
*data = queue[ConsumerStep++];
ConsumerStep%=cap;
pthread_mutex_unlock(&cmutex);
sem_post(&semSpace);
}
private:
std::vector queue;
int _cap; //循环队列容量
sem_t semSpace;
sem_t semProduct;
int ConsumerStep;
int ProducterStep;
pthread_mutex_t pmutex; //生产者间互斥
pthread_mutex_t cmutex; //消费者间互斥
};
注意点:
加锁的位置在等待信号量之后更合理。因为申请信号量并不需要锁保护,如果进行了保护,我们就无法让多个执行流同时申请到信号量,这会造成效率的浪费。
放入方法来进行测试:
#include "Task.hpp"
#include "RingQueue.hpp"
#include
#include
#include
#include
void* Consumer(void* _rq)
{
//RingQueue* rq = reinterpret_cast*>(_rq)
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) //消费
{
Task t;
rq->Pop(&t);
printf("消费者线程%0X :处理任务",pthread_self());
t();
}
return nullptr;
}
void* Producter(void* _rq)
{
//RingQueue* rq = reinterpret_cast*>(_rq);
RingQueue* rq = reinterpret_cast*>(_rq);
while(true) // 生产
{
int x = rand()%10+1;
int y = rand()%20+1;
char op = "+-*/%"[rand()%5];
Task t1(x,y,op);
rq->Push(t1);
printf("生产者线程%0X :生产了任务%d%c%d=?\n",pthread_self(),x,op,y);
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ pthread_self()); //生产随机数种子
//RingQueue* rq = new RingQueue();
RingQueue* rq = new RingQueue();
pthread_t Cpid[4];
pthread_t Ppid[4];
for(int i=0;i<4;++i) //创建一批消费者线程
{
pthread_create(&Cpid[i],nullptr,Consumer,rq);
}
for(int i=0;i<4;++i) //创建一批生产者线程
{
pthread_create(&Ppid[i],nullptr,Producter,rq);
}
for(int i=0;i<4;++i)
{
pthread_join(Cpid[i],nullptr);
pthread_join(Ppid[i],nullptr);
}
return 0;
}
此时每当我们用多个线程生产一部分任务,就会有多个线程消费一部分任务。
生产者消费者模型到底高效在哪里?
相信不少人会有这种疑问,不过就是单纯的让执行流将任务放在容器里,然后再让执行流从容器中取出任务来处理。
这是我们应该想到一个问题:任务是从哪里来,任务如何进行处理?
如果任务很大,接收任务和处理该任务无疑需要花费大量的时间。生产消费者模型高效的点就在于我们可以让多个执行流并行的去接收任务和处理任务,这样就能够让我们的效率得到很大的提升。
生产消费者模型的高效之处: 可以在生产之前和消费之后,让线程并行的去执行。