目录
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操作中我们也可以知道临界资源的情况。