目录
写在前面的话
什么是POSIX信号量
POSIX信号量的使用
基于环形队列的生产消费者模型
写在前面的话
本文章主要先介绍POSIX信号量,以及一些接口的使用,然后再编码设计一个基于环形队列的生产消费者模型来使用这些接口。
讲解POSIX信号量时,首先需要对信号量有一定的了解,大家可以去看我的这一篇文章:systemV信号量,文章的前面我详细的说明了什么是信号量以及对它的理解。
什么是POSIX信号量
POSIX信号量(POSIX semaphore)是一种线程同步机制,用于管理共享资源的并发访问。POSIX信号量是基于POSIX标准定义的一组函数和数据类型,旨在提供跨平台的线程同步能力。
POSIX信号量允许线程在访问共享资源之前获取一个信号量,通过增加或减少信号量值来控制对资源的访问。当信号量值大于零时,线程可以获取资源并继续执行。当信号量值为零时,线程将被阻塞,直到其他线程释放资源并增加信号量的值。这样可以有效地实现对共享资源的互斥访问和线程之间的同步。
POSIX信号量的使用
sem_init()
:用于初始化一个信号量对象。
函数原型如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- pshared:0表示线程间共享,非零表示进程间共享
- value:信号量初始值
sem_destroy()
:用于销毁一个信号量对象。
函数原型如下:
int sem_destroy(sem_t *sem)
sem_wait()
:尝试获取一个信号量,如果信号量值大于零,则将其减少并继续执行;否则,线程将被阻塞。
函数原型如下:
int sem_wait(sem_t *sem); //P()
sem_post()
:释放一个信号量,将其值增加,唤醒可能正在等待该信号量的线程。
函数原型如下:
int sem_post(sem_t *sem);//V()
sem_trywait()
:与sem_wait()
类似,但是尝试获取信号量时不阻塞线程,而是立即返回。sem_timedwait()
:与sem_wait()
类似,但是可以设置超时时间,如果在超时时间内仍无法获取信号量,则返回错误。
以上两个函数了解即可.
基于环形队列的生产消费者模型
这个相当于改变了交易场所,由阻塞队列变成了 环形队列.
下面是利用环形队列的几种策略:
- 环形队列采用数组模拟,用模运算来模拟环状特性
- 但是上面的设计有一个问题,假设head生产,tail消费;就是当head和tail重合的时候,我们不知道到底是head == tail还是tail == head, 即不知道此时环形队列为空还是为满。所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态,这个原理大家可以去网上查找环形队列的相关知识,这个置空位恰好将head和tail隔开。
- 但是现在有了信号量这个计数器,所以也能轻松地实现 线程间利用环形队列同步的过程。
整体代码设计如下:
首先用类设计一个环形队列RingQueue,并封装一些接口push()和pop(),成员变量为一个vector数组(用数组模拟实现环形队列)、int num_ 用来表示环形队列的大小,c_step表示消费者的下标,p_step表示生产者的下标,然后有两个信号量space_sem_和data_sem_,分别表示空间资源信号量和数据资源信号量。一开始的时候没有数据,所有空间都可使用,所以我们将空间资源信号量space_sem_设置为环形队列的长度n,data_sem_设置为0,表示没有数据。
信号量的数据类型为sem_t,但是为了方便,我对信号量进行了一个封装,类Sem,里面包含了信号量的初始化,p()和v()操作等,然后上面两个信号量的类型就直接使用Sem.
然后在RingQueue类中,两个接口push和pop的实现逻辑如下:
- push():这是生产者所使用的,生产者关注的是空间资源,如果有空间就生产,没有就停止。 生产后空间资源信号量space_sem_-1,但是数据资源信号量data_sem_+1,然后每次就在对应位置上写入相应的数据即可,记得模上数组的长度,因为逻辑结构是一个环形队列。
- pop():这是消费者所使用的,消费者关注的是数据资源,有数据就消费,没数据就不能消费。消费后数据资源信号量data_sem_-1,但是空间资源信号量space_sem+1,同样地将对应位置的数据取出即可。
所以环形队列RingQueue类和Sem类的代码如下:
Ringqueue.hpp类
#pragma once #include<iostream> #include<pthread.h> #include<vector> #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_(g_default_num),num_(g_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(); } private: vector<T> ring_queue_; int num_; int c_step;//消费者下标 int p_step;//生产者下标 Sem space_sem_; Sem data_sem_; };
Sem.hpp类
#pragma once #include "ringQueue.hpp" #include <semaphore.h> class Sem { public: Sem(int val) { sem_init(&sem_,0,val); } void p() { sem_wait(&sem_); } void v() { sem_post(&sem_); } ~Sem() { sem_destroy(&sem_); } private: sem_t sem_; };
然后我们对代码进行测试,测试代码如下,和上一节的测试代码几乎一样:
#include "ringQueue.hpp" #include<sys/types.h> #include<unistd.h> #include <time.h> using namespace std; void* consumer(void* args) { RingQueue<int>* rq = (RingQueue<int>*)args; while(true) { int x; //1.从环形队列中拿取数据 rq->pop(&x); //2.进行一定的处理 cout << "消费: " << x << endl; } } void* producter(void* args) { RingQueue<int>* rq = (RingQueue<int>*)args; while ((true)) { //1.构建数据或任务对象 -- 一般可以从外部来,不要忽略时间消耗问题 int x = rand() % 100 + 1; cout << "生产: " << x << endl; //2.推送到环形队列中 rq->push(x);//完成生产的过程 // sleep(1); } } int main() { srand((unsigned int)time(nullptr) ^ getpid() ^ 12366 ); RingQueue<int>* rq = new RingQueue<int>(); pthread_t c,p; // rq->debug(); pthread_create(&c,nullptr,consumer,(void*)rq); pthread_create(&p,nullptr,producter,(void*)rq); pthread_join(c,nullptr); pthread_join(p,nullptr); return 0; }
代码也成功的执行了:
上面是单线程,即单生产者,单消费者。
我们也改成多线程并发执行的,这也是生产消费者模型的意义所在。
当多线程并发执行时,如果两个线程 同时访问临界资源可能出错,所以需要在临界资源前后加上锁,使得只能有一个线程可以访问临界资源。
但是这样和单线程有什么区别啊,都是单线程访问,多线程的意义在哪里?
首先不要狭隘的认为,把任务或数据放在交易场所就是生产和消费了。将数据或任务拿到之后的处理,才是最耗费时间的,虽然拿的时候是加锁一个个拿的,但是处理的时候,却是一起处理的!所以生产消费者模型主要意义体现在可以并发的处理任务。
- 生产的本质:私有的任务 -> 公共空间中
- 消费的本质:公共空间中 -> 私有的
信号量本质是一把计数器 那计数器的意义是什么?
可以不用进入临界区,就可以得知资源的情况,甚至可以减少临界区内部的判断。
申请锁 -> 判断临界资源和访问 -> 释放锁 ---> 本质我们并不清楚临界资源的情况,信号量要提前预设资源的情况,而且在pv变化中,我们在外部就可以知道临界资源的情况.
所以我们在RingQueue类中,加入两个锁,分别为生产者和消费者:
主函数中,创建多个线程:
运行后,可以发现不同的线程生产和消费任务了。
这便是本章的全部内容了,主要讲述了POSIX信号量,即基于环形队列的生产消费者模型的一个实现。