【Linux】POSIX信号量基于环形队列的生产消费模型
需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
文章目录
- 引入
- 1. POSIX信号量
- 1.1 信号量的概念
- 1.2 信号量的使用
- 1.2.1 信号量的初始化
- 1.2.2信号量的销毁
- 1.2.3 等待信号量(P操作)
- 1.2.4 发布信号量(V操作)
- 2. 基于环形队列的生产消费模型
- 2.1 环形队列的概念
- 2.2 使用环形队列和信号量来实现生产消费模型
- 2.2.1 实现前的一些思考和注意事项
- 2.2.2 代码实现与测试
引入
在我们之前实现的BlockQueue中,实际上是存在一些缺陷的,可以进行一些优化
一个线程,在操作临界资源的时候,临界资源一定要是满足指定条件的,但是临界资源是否满足条件,我们在没有访问之前是无法得知的,所以只能先加锁、再检测、再操作、再解锁
所以检测是必不可少的,检测的本质也是再访问临界资源,由于我们对资源整体进行了加锁,就默认了我们对资源的整体使用,但是实际上也有可能存在对于同一份公共资源,是允许同时访问不同区域的,这个可以由程序员通过编码来控制不同区域的访问。
有没有一种方法能够提前得知当前情况下临界资源是否满足指定条件,直接让线程等待或者访问,当然是可以的,这就需要用到**信号量**
1. POSIX信号量
1.1 信号量的概念
在之前的文章中我们提到过信号量【Linux】进程间通信——system V 共享内存、消息队列、信号量 但是当时讲的是system V版本的信号量。这里是POSIX信号量。POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
那么什么是信号量呢?
-
信号量的本质就是一个计数器,用于衡量临界资源中资源数量多少的计数器
-
只要拥有信号量,就表示在未来一定能够拥有临界资源的一部分
申请信号量的本质就是**对临界资源中的特定的一小部分资源的预定**
工作机制:
用信号量表示当前临界资源被划分后的小块的个数,任何线程在使用临界资源之前都先对信号量进行申请,如果申请到信号量就表示能够访问到一部分临界资源,如果没有申请到就表示当前条件不就绪,线程需要进行等待
根据工作机制的分析,我们知道,信号量这个计数器需要进行自增和自减的操作
但是想一个问题:线程在访问临界资源之前都需要申请信号量 ==> 线程都要看到这个信号量 ==> 信号量本身就是一个临界资源 ==> 信号量的操作一定要是原子的
而事实和我们的推理结论是一致的,信号量的自增和自减都是原子的操作
-
其中信号量自减的操作就表示申请资源,也叫做P操作;信号量自增的操作就表示释放资源,也叫做V操作
-
信号量的核心操作也就是:PV原语
-
如果信号量的值为1,也就可以实现互斥锁
1.2 信号量的使用
1.2.1 信号量的初始化
头文件:
#include <semaphore.h>
函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数解释:
sem:要初始化的信号量
pshared:该信号量的共享方式,0表示线程间共享,非零表示进程间共享
value:信号量的初值(临界资源的个数)
函数描述:
初始化一个信号量
返回值:
调用成功返回0,否则返回-1同时设置错误码
1.2.2信号量的销毁
头文件:
#include <semaphore.h>
函数原型:
int sem_destroy(sem_t *sem);
参数解释:
sem:要释放的信号量
函数描述:
释放一个信号量
返回值:
调用成功返回0,否则返回-1同时设置错误码
1.2.3 等待信号量(P操作)
头文件:
#include <semaphore.h>
函数原型:
int sem_wait(sem_t *sem);
参数解释:
sem:要等待(申请)的信号量
函数描述:
申请一个信号量,如果当前信号量无法被申请,就自动挂起,等待能够被申请的时候再申请。调用成功后会将信号量的值减一
返回值:
调用成功返回0,否则返回-1同时设置错误码
1.2.4 发布信号量(V操作)
头文件:
#include <semaphore.h>
函数原型:
int sem_post(sem_t *sem);
参数解释:
sem:需要发布(释放)的信号量
函数描述:
释放一个信号量
返回值:
调用成功返回0,否则返回-1同时设置错误码
2. 基于环形队列的生产消费模型
2.1 环形队列的概念
环形队列之前我们就了解过了,只要是环形队列,就存在判空判满的问题。实际上并不是真正的环形队列,而是通过数组模拟的,当数据加入到最后的位置时直接模数组的大小即可。通常情况下,判空判满的问题我们是通过空出一个位置,当两个指针指向同一个位置的时候是空,当只剩一个位置的时候就是满,但是我们这里不需要关注。
2.2 使用环形队列和信号量来实现生产消费模型
2.2.1 实现前的一些思考和注意事项
我们在上一节的时候讲到,要维护生产者消费者模型,就需要维护“321原则”,也就是三种关系,两个角色,一个交易场所
在环形队列中,三个关系就是生产者与生产者之前的关系(互斥),消费者与消费者之间的关系(互斥),生产者与消费者之间的关系(无关)
两个角色:生产者和消费者
一个交易场所:环形队列
但是,由于环形队列的特殊结构,其中的任意一个位置都可以被访问到,所以一个生产者和一个消费者可以同时访问这个队列中的不同位置,只有生产者和消费者处于同一个位置的时候才需要考虑互斥与同步的问题
所以我们的核心工作就是
- 消费者不能超过生产者
- 生产者不能给消费者套圈
- 当生产者和消费者处于同一位置的时候,如果此时队列为空就让生产者先走,如果此时队列为满就让消费者先走
那么信号量所表示的资源又如何理解呢?
对于生产者,他只关注这个队列中的剩余空间,因为他需要空间来存放生产的结果
对于消费者,他只需要关注这个队列的数据资源,因为需要消耗数据
所以最终我们定义两个信号量,分别表示剩余空间和已有的数据
如何维护生产者和生产者的关系,消费者和消费者的关系
这里各个生产者之间需要互斥,消费者也需要互斥,但是生产者和消费者之间没有任何关系,最终为了维护这种互斥关系,我们就需要两个不相关的锁分别管理生产者和消费者
2.2.2 代码实现与测试
/*RingQueue.hpp*/
#pragma once
#include <vector>
#include <cassert>
#include <semaphore.h>
#include <pthread.h>
const int gnum = 5; // 默认的环形队列大小
template <class T>
class RingQueue
{
public:
RingQueue(const int &num = gnum) : _queue(num) // 构造函数
{
// 初始化两个信号量
int n = sem_init(&_spaceSem, 0, num);
assert(n == 0);
n = sem_init(&_dataSem, 0, 0);
assert(0 == n);
_productorStep = _consumerStep = 0; // 初始化生产者和消费者需要访问的位置
}
void push(const T &in) // 生产者使用空间
{
// 申请信号量之前,数据已经并发的生产完成
P(_spaceSem); // 在向队列里面push数据之前申请_spaceSem信号量
pthread_mutex_lock(&_pmutex); // 在申请信号量之后考虑多个生产线程的互斥访问的问题
_queue[_productorStep++] = in; // 申请到信号量之后队列中指定位置插入数据
_productorStep %= _queue.size(); // 更新生产者下标
pthread_mutex_unlock(&_pmutex); // 解锁
V(_dataSem); // 插入数据之后,可使用的数据就要增加,所以使用V操作释放_dataSem
}
void pop(T *out) // 消费者使用数据
{
P(_dataSem);
pthread_mutex_lock(&_cmutex);
*out = _queue[_consumerStep++];
_consumerStep %= _queue.size();
pthread_mutex_unlock(&_cmutex);
V(_spaceSem);
}
~RingQueue() // 析构函数
{
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
}
private:
// 封装成私有的PV操作
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
private:
std::vector<T> _queue;
sem_t _spaceSem; // 生产者看重的剩余空间信号量
sem_t _dataSem; // 消费者看重的存在的数据个数信号量
int _productorStep; // 生产者要访问的下标
int _consumerStep; // 消费者要访问的下标
pthread_mutex_t _pmutex; // 维护生产者关系的锁
pthread_mutex_t _cmutex; // 维护消费者关系的锁
};
测试1:使用整数测试单生产单消费
#include "RingQueue.hpp"
#include <cstdlib>
#include <ctime>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
void *productor(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while(true)
{
//生产数据
int data = rand() % 10 + 1;
rq->push(data);
std::cout << "生产数据成功,生产的数据是" << data << std::endl;
// sleep(1); // 让生产慢一点
}
}
void *consumer(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while(true)
{
//消费数据
int data;
rq->pop(&data);
std::cout << "消费数据成功,消费的数据是" << data << std::endl;
sleep(1); // 让消费慢一点
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid()); // 种随机数种子,为了让产生的随机数更随机
RingQueue<int> *rq = new RingQueue<int>();
pthread_t p,c;
pthread_create(&p, nullptr, productor, rq);
pthread_create(&c, nullptr, consumer, rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
测试2:使用Task测试单生产单消费
/*Task*/
#pragma once
#include <string>
#include <iostream>
#include <functional>
static std::string oper = "+-*/%";
class CalTask
{
public:
using func_t = std::function<int(int, int, char)>;
public:
CalTask() {}
CalTask(int x, int y, char op, func_t func)
: _x(x), _y(y), _op(op), _callback(func)
{
}
std::string operator()()
{
int result = _callback(_x, _y, _op);
char buffer[64];
snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
return buffer;
}
std::string toTaskString()
{
char buffer[64];
snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
return buffer;
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
class SaveTask
{
typedef std::function<void(const std::string)> func_t;
public:
SaveTask() {}
SaveTask(const std::string &msg, func_t func)
: _msg(msg), _func(func)
{
}
void operator()()
{
_func(_msg);
}
private:
std::string _msg;
func_t _func;
};
int mymath(int a, int b, char op)
{
int ans = 0;
switch (op)
{
case '+':
ans = a + b;
break;
case '-':
ans = a - b;
break;
case '*':
ans = a * b;
break;
case '/':
{
if (b == 0)
{
std::cerr << "div zero error!" << std::endl;
ans = -1;
}
else
ans = a / b;
}
break;
case '%':
{
if (b == 0)
{
std::cerr << "mod zero error!" << std::endl;
ans = -1;
}
else
ans = a % b;
}
break;
default:
break;
}
return ans;
}
void Save(const std::string &msg)
{
FILE *fp = fopen("./log.txt", "a+");
if (fp == NULL)
{
std::cerr << "open file error" << std::endl;
return;
}
fputs(msg.c_str(), fp);
fputs("\n", fp);
fclose(fp);
}
/*main.cc*/
#include "RingQueue.hpp"
#include "Task.hpp"
#include <cstdlib>
#include <ctime>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
void *productor(void *args)
{
RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);
while(true)
{
//生产数据
int x = rand() % 10 + 1;
int y = rand() % 10 + 1;
int opNum = rand() % oper.size();
CalTask t(x, y, oper[opNum], mymath);
rq->push(t);
std::cout << "生产数据成功" << t.toTaskString() << std::endl;
sleep(1); // 让生产慢一点
}
}
void *consumer(void *args)
{
RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);
while(true)
{
//消费数据
CalTask t;
rq->pop(&t);
std::string result = t();
std::cout << "消费数据成功 " << result << std::endl;
// sleep(1); // 让消费慢一点
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid()); // 种随机数种子,为了让产生的随机数更随机
RingQueue<CalTask> *rq = new RingQueue<CalTask>();
pthread_t p,c;
pthread_create(&p, nullptr, productor, rq);
pthread_create(&c, nullptr, consumer, rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
测试3:多生产多消费的测试
#include "RingQueue.hpp"
#include "Task.hpp"
#include <cstdlib>
#include <ctime>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
void *productor(void *args)
{
RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);
while(true)
{
//生产数据
int x = rand() % 10 + 1;
int y = rand() % 10 + 1;
int opNum = rand() % oper.size();
CalTask t(x, y, oper[opNum], mymath);
rq->push(t);
std::cout << "生产数据成功" << t.toTaskString() << std::endl;
sleep(1); // 让生产慢一点
}
}
void *consumer(void *args)
{
RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);
while(true)
{
//消费数据
CalTask t;
rq->pop(&t);
std::string result = t();
std::cout << "消费数据成功 " << result << std::endl;
// sleep(1); // 让消费慢一点
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid()); // 种随机数种子,为了让产生的随机数更随机
RingQueue<CalTask> *rq = new RingQueue<CalTask>();
pthread_t p[4],c[8];
for(int i = 0; i < 4; ++i) pthread_create(p + i, nullptr, productor, rq);
for(int i = 0; i < 8; ++i) pthread_create(c + i, nullptr, consumer, rq);
for(int i = 0; i < 4; ++i) pthread_join(p[i], nullptr);
for(int i = 0; i < 8; ++i) pthread_join(c[i], nullptr);
return 0;
}
多生产多消费的意义:不管是环形队列还是阻塞队列,多线程的意义在于构建or获取任务是要花时间的,效率比较低,当消费的时候也是要花时间的,不单单只是拿出来就行了,所以多生产多消费的时候的意义在于生产之前,消费之后,处理任务获取任务的时候本身也是要花费时间的,可以在生产之前与消费之后让线程并行执行。
条件变量是一种同步机制,它允许线程等待某个条件的发生,通常与互斥锁一起使用。而信号量是一种计数器,它可以用于控制对共享资源的访问;如果想让每一刻只有一个线程访问共享资源,可以使用条件变量。但如果需要允许多个线程并发访问共享资源的不同区域,则可以使用信号量
本节完…