什么是信号量?
本质信号量就是一个计数器,它表示临界资源的数量,也就是说它描述的是有多少临界资源可以分配给线程去访问;
对于临界资源来说,假如我们可以把它在细分多个小的资源区域,如果我们有某总手段处理得当,也是可以让多个线程同时访问临界资源的不同区域,从而实现并发的效果;
每个线程在访问临界资源时候,首先必须先申请信号量资源,申请成功才可以进入临界资源;
一旦申请成功进入临界资源区域,那么也就表示内部一定有一个小区域是可以给你线程访问的;
总的来说:信号量本质就是一把计数器,能够达到对临界资源预定的目的;
如何保证信号量是原子的操作呢?只需要加锁就可以完美解决这个问题;
都说信号量本质是计数器,线程进入临界区资源都需要先申请信号量资源,申请信号量资源就是对齐信号进行-1操作,而这个-1的操作本身不是原子的,为了保证原子就必须加锁;而我们的信号量资源可能不被申请成功,那么该线程就必须挂起等待了;
同理当线程离开临界资源区时候,要归还信号量资源,也就是对其+1操作,但是这个操作也不是原子的,所以也必须加锁!
那么如何实现呢?实现的基本思想:
对于生产者最关心的是什么?关心的是环形队列的空位置有多少。
对于消费者最关心的是什么?关心的是环形队列的数据(产品)有多少。
所以我们来实现一下代码:
首先有两个文件一个是头文件:ring_queue.hpp
一个是主程序的文件:mian.cc
;
头文件:我们使用的是C++的vector
来模拟环形队列;
初始情况:
我们这里只维护了生产者和消费者之间的竞争关系和同步关系;
因为现在我们模拟的是一个生产者和一个消费者的情况;
环形队列为空,也就是没有数据,也就是有很多空格子,而空格子就是生产者关心的资源数量;
我们初始化空格子的数量为10;也就是信号量设置10,表示生产者的资源数量;
而数据的信号量设置为0,因为一开始是没有数据的;
首先是ring_queue.hpp
文件:
#pragma once
#include
#include
const int _g_cap_default = 10;
template <typename T>
class RingQueue
{
private:
std::vector<T> _ring_queue;
int _cap;
/*
//生产者和消费者之间关系:竞争和同步
如何维护呢?
搞两个信号量
1.生产者只关心临界区里面空格子的数量,因为生产者需要放数据
2.消费者值关心理解去里面数据的数量 ,因为消费者需要消费数据
*/
sem_t _blanks_sem; //生产者关心的临界资源是空格子的数目
sem_t _data_sem; // 消费者关心的临界资源是数据
int _producer_index; //生产者的环形队列下标
int _consumer_index; //消费者的环形队列下标
public:
RingQueue(int cap = _g_cap_default) : _ring_queue(cap), _cap(cap)
{
sem_init(&_blanks_sem, 0, cap); // 生产者刚开始有的格子的个数
sem_init(&_data_sem, 0, 0); // 消费者刚开始是没有数据消费的
_producer_index = 0;
_consumer_index = 0;
}
~RingQueue()
{
sem_destroy(&_blanks_sem);
sem_destroy(&_data_sem);
}
public:
void push(const T &in)
{
//生产者生产数据第一件事是:申请临界区的资源
sem_wait(&_blanks_sem); // p(空格子)
_ring_queue[_producer_index] = in;
//生产者生产完数据后,就可以增加消费者的数据资源啦!
sem_post(&_data_sem); // V(数据)
_producer_index++;
_producer_index %= _cap;
}
void pop(T *out)
{
//消费数据的第一件事:申请临界区的资源
sem_wait(&_data_sem);
*out = _ring_queue[_consumer_index];
//消费者消费完数据,就可以增加生产者的空格子资源啦!
sem_post(&_blanks_sem);
_consumer_index++;
_consumer_index %= _cap;
}
};
主程序:main.cc
#include
#include
#include "ring_queue.hpp"
#include
#include
using namespace std;
void *consumer(void *args)
{
RingQueue<int> *ring_queue = (RingQueue<int> *)args;
while (true)
{
sleep(1);
int data = 0;
ring_queue->pop(&data);
cout << "消费者线程: " << pthread_self() << "消费的的数据是:" << data << endl;
}
}
void *producer(void *args)
{
RingQueue<int> *ring_queue = (RingQueue<int> *)args;
while (true)
{
//生产者生产产品的数据来源:这个场景就是随机数
int data = rand() % 20 + 1;
cout << "生产者线程:" << pthread_self() << "生产的数据是:" << data << endl;
ring_queue->push(data);
}
}
int main()
{
srand((long long)time(NULL));
RingQueue<int> *ring_queue = new RingQueue<int>();
pthread_t comsumer_tid;
pthread_t producer_tid;
pthread_create(&producer_tid, NULL, producer, (void *)ring_queue);
pthread_create(&comsumer_tid, NULL, consumer, (void *)ring_queue);
pthread_join(producer_tid, nullptr);
pthread_join(comsumer_tid, nullptr);
return 0;
}
执行的结果:
由于我设计了消费者线程先睡一秒,所以生产者就先获得CPU调度,马上生产完了一批数据,然后消费者来消费,然后就一步一步的运行了;
上面的只是完成了一个生产者和一个消费者的生产者消费者模型;
但是实际上:我们是有多个生产者和多个消费者的;
所以我们还需要维护生产者和生产者的竞争关系,消费者和消费者的竞争关系;
如何维护呢?
只要加锁就可以解决;
所以我们修改了上面的ring.queue.hpp的代码
#pragma once
#include
#include
#include
const int _g_cap_default = 10;
template <typename T>
class RingQueue
{
private:
std::vector<T> _ring_queue;
int _cap;
/*
//生产者和消费者之间关系:竞争和同步
如何维护呢?
搞两个信号量
1.生产者只关心临界区里面空格子的数量,因为生产者需要放数据
2.消费者值关心理解去里面数据的数量 ,因为消费者需要消费数据
*/
sem_t _blanks_sem; //生产者关心的临界资源是空格子的数目
sem_t _data_sem; // 消费者关心的临界资源是数据
/*
在多生产多消费线程的模型下
_producer_index和_consumer_index也会成为临界资源
_producer_index 是生产者和生产者之间的临界资源
_consumer_index 是消费者和消费者之间的临界资源
*/
int _producer_index; //生产者的环形队列下标
int _consumer_index; //消费者的环形队列下标
pthread_mutex_t _consumer_mutex; //维护消费者和消费者之间线程竞争资源的锁
pthread_mutex_t _producer_mutex; //维护生产者和生产者之间线程竞争资源的锁
public:
RingQueue(int cap = _g_cap_default) : _ring_queue(cap), _cap(cap)
{
sem_init(&_blanks_sem, 0, cap); // 生产者刚开始有的格子的个数
sem_init(&_data_sem, 0, 0); // 消费者刚开始是没有数据消费的
_producer_index = 0;
_consumer_index = 0;
pthread_mutex_init(&_consumer_mutex, NULL);
pthread_mutex_init(&_producer_mutex, NULL);
}
~RingQueue()
{
sem_destroy(&_blanks_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&_consumer_mutex);
pthread_mutex_destroy(&_producer_mutex);
}
public:
void push(const T &in)
{
/*
信号量解决的是:临界资源的数目还剩多少
锁解决的是:保证临界资源的生产者只有一个在访问
*/
//生产者生产数据第一件事是:申请临界区的资源
sem_wait(&_blanks_sem); // p(空格子)
pthread_mutex_lock(&_producer_mutex); //这个锁主要是解决生产者和生产者之间的竞争关系的
_ring_queue[_producer_index] = in;
_producer_index++;
_producer_index %= _cap;
pthread_mutex_unlock(&_producer_mutex);
//生产者生产完数据后,就可以增加消费者的数据资源啦!
sem_post(&_data_sem); // V(数据)
}
void pop(T *out)
{
/*
信号量解决的是:临界资源的数目还剩多少
锁解决的是:保证临界资源的消费者只有一个在访问
*/
//消费数据的第一件事:申请临界区的资源
sem_wait(&_data_sem);
pthread_mutex_lock(&_consumer_mutex); //这个锁主要是解决消费者和消费者之间的竞争关系的
*out = _ring_queue[_consumer_index];
_consumer_index++;
_consumer_index %= _cap;
pthread_mutex_unlock(&_consumer_mutex);
//消费者消费完数据,就可以增加生产者的空格子资源啦!
sem_post(&_blanks_sem);
}
};
最主要的修改部分:就是加多了一把锁维护了生产者和生产者之间竞争关系;消费者和消费者之间的竞争关系;
因为当有多生产者的时候,每个生产者就会同时访问临界资源,需要对齐进行互斥访问;
消费者同理;
为什么这里的P操作时在上锁的操作之前?
因为我们知道P操作就是对访问临界资源起了一个预定的机制;当有多生产者进来时候,我们多个生产者都有机会预定访问临界资源,但是只有一个生产者有资格进入临界资源;
而我们的mian.cc
的代码主要从原来的单生产单消费变成了多生产多消费;
这里模拟了3个生产三个消费的线程;
执行的结果如下:
发现确实时有3个生产者和3个消费者并发的进行了;
还是那句话:生产者和消费者模型;关心的不仅仅是如何把数据放入队列,和如何从队列拿出数据;
我们更加要关心:数据到来生产者的时间和消费者拿出数据处理的时间问题;
上面的代码示例我都没演示:数据到来生产者的时间和消费者拿出数据处理的时间问题;
只是演示如何把数据放入队列,和如何从队列拿出数据;
其实只要把上面的的main.cpp的代码稍微改一下就可以了,我们知道不往队列放入整数,比如放入一个任务,那么这个任务就会有数据来源和数据处理的过程了!!!!