POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
信号量的**本质就是一个计数器,而计数器的本质是用来描述我们的资源数目.**当我们申请信号量时,就是在判断我们是否有资源,加上锁的保护,就可以实现我们多线程的安全操作。、
信号量函数
sem_init (初始化信号量)
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem_t:头文件内封装的信号类型
pshared:0 表示线程间共享,非零表示进程间共享
value :信号量初始值
sem _destroy 摧毁信号量
#include <semaphore.h>int sem_destroy(sem_t *sem);
参数:
sem:信号量变量
sem_wait 等待信号量
功能:等待信号量,会将信号量的值减 1,若信号量为零,则挂起等待
int sem_wait(sem_t *sem);
sem_post 发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加 1,自动唤醒挂起的线程
int sem_post(sem_t *sem);
了解了信号量的作用以及他的使用函数之后,我们利用信号量来实现一个唤醒队列的生产消费模型的实例。
基于环形队列的生产消费模型
由于我们使用的是环形队列,所以我们希望在该消费模型中将我们的生产和消费的效率最大化,我们可以采用生产和消费同步进行。只要生产者和消费者不是在同一个位置进行生产和消费,我们就可以让他们同时进行。
我们来分析会出现的几种情况:
1、生产者和生产者之间:当我们环形队列没有被填满时,我们的生产者之间是互斥的关系,我在进行生产的时候,你就不可以来抢占我的位置(规定一次只能生产一位置的数据)。当填满时,就不可以在进行生产,需要进行等待。
2、消费者与消费者之间:消费者之间和生产者之间的关系是类似的互斥关系
3、生产者和消费者之间:当我们的环形队列为空时,消费者和生产者都在同一位置等待,此时他们二者是互斥的,并且只能让生产者执行,当环形队列不满时,我们的生产者和消费者之间是可以同时进行的,因为他们并没有在同一个位置上,当我们的队列满了时,生产者和消费者是互斥的,并且只能让消费者执行。
有了上述的分析,我们就可以很轻松的实现我们的CP模型啦。
重点还是在于我们的交易场所,代码如下:
cpp
#pragma once
#include <vector>
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
using namespace std;
#define defaultnum 5
template <class T>
class Ringqueue
{
private:
void P(sem_t &sem)
{
sem_wait(&sem);
}
void V(sem_t &sem)
{
sem_post(&sem);
}
void Lock(pthread_mutex_t &mutex)
{
pthread_mutex_lock(&mutex);
}
void Unlock(pthread_mutex_t &mutex)
{
pthread_mutex_unlock(&mutex);
}
public:
Ringqueue(int cap = defaultnum) : cap_(cap),ringqueue_(cap),c_step_(0),p_step_(0)
{
sem_init(&cdata_sem_, 0, 0);
sem_init(&pspace_sem_, 0, cap_);
pthread_mutex_init(&c_mutex_, nullptr);
pthread_mutex_init(&p_mutex_, nullptr);
}
void Push(const T&in)
{
P(pspace_sem_);
Lock(p_mutex_);
ringqueue_[p_step_] = in;
p_step_++;
p_step_%=cap_;
Unlock(p_mutex_);
V(cdata_sem_);
}
void Pop(T* out)
{
P(cdata_sem_);
Lock(c_mutex_);
*out = ringqueue_[c_step_];
c_step_++;
c_step_ %= cap_;
Unlock(c_mutex_);
V(pspace_sem_);
}
~Ringqueue()
{
sem_destroy(&cdata_sem_);
sem_destroy(&pspace_sem_);
pthread_mutex_destroy(&c_mutex_);
pthread_mutex_destroy(&p_mutex_);
}
private:
vector<T> ringqueue_;
int cap_;
int c_step_;
int p_step_;
sem_t cdata_sem_; // 消费者的信号量
sem_t pspace_sem_; // 生产者的信号量
pthread_mutex_t c_mutex_; // 消费的锁
pthread_mutex_t p_mutex_; // 生产者的锁
};
变量设计:
- vector<T>ringqueue_:用STL提供的vector来模拟实现环形队列。
- **int cap_ :**用于控制我们的环形队列的长度
- **int c_step_:**用于标记我们的消费者消费到了那个位置。
- **int p_step_:**用于标记我们的生产者生产到了哪个位置。
- sem_t cdata_sem_: 消费者的信号量,用于申请消费资源
- sem_t pspace_sem_: 生产者的信号量,用于申请生产资源
- **pthread_mutex_t c_mutex_:**消费者的锁,防止多消费者
- pthread_mutex_t p_mutex_: 生产者的锁,防止多生产者
函数设计
**封装接口:**简单封装系统调用接口
//这些函数都是为了简单封装我们的系统调用接口
void P(sem_t &sem) //体现我们在信号量中的P操作
{
sem_wait(&sem);
}
void V(sem_t &sem)//体现我们在信号量中的V操作
{
sem_post(&sem);
}
void Lock(pthread_mutex_t &mutex)//封装加锁
{
pthread_mutex_lock(&mutex);
}
void Unlock(pthread_mutex_t &mutex)//封装解锁
{
pthread_mutex_unlock(&mutex);
}
构造函数
构造函数可接受用户自定传入的环形队列大小,同时将我们的生产则和和消费者的下标都置为0,同时初始化我们生产者和消费者的信号量和锁。
Ringqueue(int cap = defaultnum) : cap_(cap),ringqueue_(cap),c_step_(0),p_step_(0)
{
sem_init(&cdata_sem_, 0, 0);
sem_init(&pspace_sem_, 0, cap_);
pthread_mutex_init(&c_mutex_, nullptr);
pthread_mutex_init(&p_mutex_, nullptr);
}
Push函数(生产资源)
当我们生产资源的时候,首先要先申请生产者的信号量(P操作),如果有才能继续申请,否则挂起等待,申请成功后,对身生产者进行加锁,生产数据,然后将下标++并取模运算,解开生产者的锁,此时我们就以及生产了一个资源,就可以保证我们的消费者一定有数据消费,所以我们可以对消费者的信号量进行释放信号量(V操作),也就是告诉消费者有数据可以来读取了,能够唤醒挂起等待的消费者
void Push(const T&in)
{
P(pspace_sem_);
Lock(p_mutex_);
ringqueue_[p_step_] = in;
p_step_++;
p_step_%=cap_;
Unlock(p_mutex_);
V(cdata_sem_);
}
Pop函数(消费资源)
当我们消费资源的时候,需要申请消费者的信号量(P操作),如果有资源则申请成功,否则申请失败,挂起等待,申请成功后,对消费者进行加锁,取出资源消费数据,然后对消费者的下标进行++并进行模运算,解开消费者的锁,此时我们以及消费了一个资源,所以就一定有空间让生产者来进行生产,所以我们可以释放生产者的信号量(V操作),让生产者直到,有位置让生产者来进行生产了。
void Pop(T* out)
{
P(cdata_sem_);
Lock(c_mutex_);
*out = ringqueue_[c_step_];
c_step_++;
c_step_ %= cap_;
Unlock(c_mutex_);
V(pspace_sem_);
}
析构函数
摧毁掉所有的锁和信号量
~Ringqueue()
{
sem_destroy(&cdata_sem_);
sem_destroy(&pspace_sem_);
pthread_mutex_destroy(&c_mutex_);
pthread_mutex_destroy(&p_mutex_);
}