目录
POSIX信号量
信号量原理
信号量概念
信号量函数
基于环形队列的生产者消费者模型
生产者和消费者申请和释放资源
单生产者单消费者
多生产者多消费者
多生产者多消费者的意义
信号量的意义
POSIX信号量
信号量原理
- 如果仅用一个互斥锁对临界资源进行保护,相当于把这块临界资源看作一个整体,同一时间只允许一个执行流对这块临界资源进行访问。
- 如果将这块临界资源再分成多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那就可以继续并发访问,访问同一个资源的时候再进行同步和互斥。
信号量概念
信号量本质上就是一个计数器,记录的是临界资源中资源数目的计数器,它可以更细粒度的对临界资源进行管理。
访问临界资源的时候,先申请信号量,信号量的值--,这叫做预定资源,我们叫做P操作;使用完后,信号量的值++,这叫做释放资源,我们叫做V操作。
当我们申请了一个信号量,可以保证当前执行流一定具有一个资源,要说是哪一个那就需要我们自己编写代码决定了。
信号量函数
作用:初始化信号量
参数:
- sem:需要初始化的信号量
- pshared:传入0表示线程间共享,传入非0表示进程间共享
- value:信号量的初始值(类似一个计数器)
返回值:成功返回0,失败返回-1,错误码被设置。
作用:销毁信号量
参数:要销毁的信号量
返回值:成功返回0,失败返回-1,错误码被设置。
作用:等待信号量
参数:要等待的信号量
返回值:
- 等待信号量成功返回0,信号量的值减一。
- 等待信号量失败返回-1,信号量的值保持不变。
作用:释放信号量
参数:需要释放的信号量
返回值:
- 发布信号量成功返回0,信号量的值加一。
- 发布信号量失败返回-1,信号量的值保持不变。
基于环形队列的生产者消费者模型
生产者最看重的就是队列中的空间,而消费者最看重的就是队列中的数据。只要有空间就可以生产数据,只要有数据就可以消费。
用信号量来表示上述的数据:
- spaceSem:表示有多少空间,开始设为N(队列长度)。
- dataSem:表示有多少数据,开始设为0。
生产者和消费者申请和释放资源
当生产数据的时候:
P(spaceSem) spaceSem--; // 生产数据 V(dataSem)dataSem++;
当消费数据的时候
P(dataSem)dataSem--; // 消费数据 V(spaceSem)spaceSem++;
当两个执行流同时访问的时候:
- 如果消费者先执行,要P(申请)dataSem(数据),但是一开始的dataSem的值是0,所以就被阻塞了。
- 如果生产者先运行,要P(申请)spaceSem(空间),一开始的spaceSem的值是N,就可以申请成功,生产数据,之后V(释放)dataSem(数据)。
- 如果生产者把数据放满了,要P(申请)spaceSem(空间)就会失败,生产者就被阻塞。
- 如果两个执行流都能获取想要的资源,那就可以实现并发访问。
单生产者单消费者
// Sem.hpp #include <iostream> #include <pthread.h> #include <semaphore.h>// 封装一下信号量 class Sem { public: Sem(int value){sem_init(&_sem, 0, value);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}~Sem(){sem_destroy(&_sem);} private:sem_t _sem; };
// ringQueue.hpp #include <iostream> #include <vector> #include <pthread.h> #include <unistd.h> #include "Sem.hpp"using namespace std;const int g_default_num = 5;template <class T> class RingQueue { public:RingQueue(int default_num = g_default_num):_ring_queue(default_num),_num(default_num),_c_step(0),_p_step(0),_space_sem(default_num),_data_sem(0){}~RingQueue(){}// 生产者void push(const T& in){_space_sem.P();_ring_queue[_p_step++] = in;_p_step %= _num;_data_sem.V();}// 消费者void pop(T* out){_data_sem.P();*out = _ring_queue[_c_step++];_c_step %= _num;_space_sem.V();}void debug(){cout << "size: " << _num << "queue: " << _ring_queue.size() << endl;}private:vector<T> _ring_queue;size_t _num;int _c_step; // 消费者下标int _p_step; // 生产者下标Sem _space_sem; // 空间信号量Sem _data_sem; // 数据信号量 };
// testMain.cc#include "ringQueue.hpp" #include <ctime>void* consumer(void* args) {RingQueue<int>* rq = (RingQueue<int>*)args;while (true){int x;// 1.从环形队列中获取任务rq->pop(&x);// 2.进行处理cout << "消费:" << x << endl;// sleep(1);} }void* productor(void* args) {RingQueue<int>* rq = (RingQueue<int>*)args;while (true){// 1.构建数据或任务对象,数据可能从任何地方来,那一定会有时间消耗int x = rand() % 10 + 1;// 2.推送到环形队列rq->push(x);cout << "生产:" << x << endl;// sleep(1);} }int main() {srand((unsigned)time(nullptr) ^ getpid());RingQueue<int>* rq = new RingQueue<int>();pthread_t c, p;pthread_create(&c, nullptr, consumer, (void*)rq);pthread_create(&p, nullptr, productor, (void*)rq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0; }
与基于阻塞队列的生产者消费者模型不同的是,阻塞队列是一块临界资源,就会有互斥和同步的问题,生产者和消费者访问临界资源的时候就要加互斥锁来保护临界资源,用信号量实现没有使用互斥锁,我们把资源的数目规定好,通过管理这些资源的数量,就可以对每一块资源更细粒度的管理。
关于环形队列的实现就不过多赘述了,就是控制下标加上模运算。生产者消费者只要访问的不是环形队列中的相同区域,他们两个基本就没有关系,所以可以实现并发访问。我们维护的只有生产者和消费者之间的互斥和同步关系。
多生产者多消费者
如果是多生产和多消费该怎么做呢?我们要知道的是相比于单生产单消费要多维护什么关系,其实就是生产和生产间、消费和消费间的这两种互斥关系。
如果只加一把锁,本来生产和消费可以有很大概率并发执行,现在又多了锁的竞争,就可能变成串行执行,一把不行,那就加两把。
生产者之间的临界资源就是空间,消费者之间的临界资源就是数据。
既然有了锁就可以保护临界资源了,那么我是先申请信号量还是先申请锁呢?假如先申请锁,锁申请成功了,再申请信号量,此时就可能有很多信号量还没有分配出去,前面我们也说过,这个信号量是一种预定机制,即便申请了信号量也没有使用资源,那为何不先申请信号量呢,所以一般都是先申请信号量再加锁。
const int g_default_num = 5;template <class T> class RingQueue { public:RingQueue(int default_num = g_default_num):_ring_queue(default_num),_num(default_num),_c_step(0),_p_step(0),_space_sem(default_num),_data_sem(0){pthread_mutex_init(&_clock, nullptr);pthread_mutex_init(&_plock, nullptr);}~RingQueue(){pthread_mutex_destroy(&_clock);pthread_mutex_destroy(&_plock);}// 生产者void push(const T& in){_space_sem.P();pthread_mutex_lock(&_plock);_ring_queue[_p_step++] = in;_p_step %= _num;pthread_mutex_unlock(&_plock);_data_sem.V();}// 消费者void pop(T* out){_data_sem.P();pthread_mutex_lock(&_clock);*out = _ring_queue[_c_step++];_c_step %= _num;pthread_mutex_unlock(&_clock);_space_sem.V();}void debug(){cout << "size: " << _num << "queue: " << _ring_queue.size() << endl;}private:vector<T> _ring_queue;size_t _num;int _c_step; // 消费者下标int _p_step; // 生产者下标Sem _space_sem; // 空间信号量Sem _data_sem; // 数据信号量pthread_mutex_t _clock; // 消费者之间的锁pthread_mutex_t _plock; // 生产者之间的锁 };
多生产者多消费者的意义
其实生产者往容器缓冲区中放数据和消费者从容器缓冲区中拿数据,就是一个生产者在放,一个消费者在拿,那它的意义在哪呢?
我们要思考的是,我们从哪里拿到的任务也就是生产任务前,我们拿到任务后该怎么做,如果只有一个执行流,它既要做这个也要做那个,中间还得加锁,那任务就是一个一个做的,如果使用多线程,那么多个线程就可以并发的处理这些动作。
信号量的意义
信号量的意义是什么呢?
看到这里是一定会带有问题的,阻塞队列时,我们要先申请锁,再检测,不成功就阻塞,唤醒后在检测,成功后再执行,但是使用信号量都没有检测,甚至可能都没有加锁。
其实阻塞队列中我们并不清楚临界资源的情况,但信号量是一个计数器,它可以预定某种资源,在PV操作中我们也可以知道临界资源的情况。