目录
[三、基于环形队列的生产者 - 消费者模型](#三、基于环形队列的生产者 - 消费者模型)
一、回顾信号量的概念
信号量本质上就是一个计数器,具体一点就是描述临界资源当中资源数目的计数器!如果资源只有一份,我们把这个信号量称为二元信号量;如果资源有多份,则称为多元信号量。由于资源只有一份,所以同时只能有一个线程访问这个资源。也就是说二元信号量和互斥锁的作用是等价的!
信号量申请(P操作,计数器--)和释放的操作(V操作,计数器++)都是原子的!
二、POSIX信号量接口
创建信号量sem_init:

1.sem_t *sem:
这是一个输出型参数,指向你要初始化的信号量对象的地址。信号量本身的类型是 sem_t,这个参数就是告诉函数要初始化的是哪一个信号量
- int pshared
这个参数决定了信号量的共享范围:为0时表示这个信号量只在同一个进程内的线程之间共享;非0则表示这个信号量在多个进程之间共享,一般需要放在共享内存中才能生效。
- unsigned int value
如果设为 1,它就是一个二元信号量,效果等价于互斥锁。
如果设为 N(N>1),它就是一个多元信号量,可以同时允许 N 个线程访问临界资源。
返回值:成功返回0,失败返回-1。
删除一个信号量sem_destroy:

返回值:成功返回0,失败返回-1。
锁住一个信号量sem_wait(P操作,计数器--):

解锁一个信号量sem_post(V操作,计数器++):

三、基于环形队列的生产者 - 消费者模型
我们用一个环形队列来做生产者和消费者之间的缓冲区。生产者(tail 指针)负责往队列里放数据;而消费者(head 指针)负责从队列里拿数据。
使用这个环形队列模型需要遵守四个规则:
1.当队列为空时,head 和 tail 指向同一个位置,此时消费者不能拿不存在的数据
2.当队列为满时,head 和 tail 指向同一个位置,此时生产者不能再往里面放数据
3.生产者不能跑太快,超过消费者整整一圈;消费者也不能超过生产者,拿还没生产的数据
4.当队列既不为空也不为满时,生产者和消费者可以同时操作,互不干扰
生产者线程逻辑伪代码:
cpp
// 1. P操作:申请空位置(blank_sem--),如果没有空位就阻塞等待
P(blank_sem);
// 2. 生产数据,放入环形队列
ring[tail++] = data;
tail %= N; // 环形队列,指针绕回
// 3. V操作:释放数据资源(data_sem++),通知消费者有数据了
V(data_sem);
消费者线程逻辑伪代码:
cpp
// 1. P操作:申请数据(data_sem--),如果没有数据就阻塞等待
P(data_sem);
// 2. 消费数据,从环形队列取出
out = ring[head++];
head %= N; // 环形队列,指针绕回
// 3. V操作:释放空位置(blank_sem++),通知生产者有空位了
V(blank_sem);
当队列为空时,data_sem=0,消费者执行P(data_sem)会阻塞,不会取空数据;此时生产者P(blank_sem)能成功执行,放入数据后V(data_sem)唤醒消费者。
当队列为满时,blank_sem=0,生产者执行P(blank_sem)会阻塞,不会覆盖数据;此时消费者P(data_sem)能成功执行,取出数据后V(blank_sem)唤醒生产者
由于P和V操作是原子的,计数不会出错。而生产者和消费者操作的是不同的信号量,不会互相干扰。当队列既不为空也不为满时,两个线程能同时操作,实现并发操作。
四、代码实现
4.1信号量的封装
Sem.hpp
cpp
#ifndef __SEM_HPP
#define __SEM_HPP
#include <iostream>
#include <semaphore.h>
class Sem
{
public:
// 构造函数:初始化信号量
Sem(int init_val)
{
if (init_val >= 0)
{
int n = sem_init(&_sem, 0, init_val);
(void)n; // 消除变量未使用警告
}
}
// P操作(申请资源/等待)
void P()
{
//sem_wait会让信号量值减1,如果值为0则阻塞等待
int n = sem_wait(&_sem);
(void)n;
}
// V操作(释放资源/唤醒)
void V()
{
// sem_post 会让信号量值加1,并唤醒等待的线程
int n = sem_post(&_sem);
(void)n;
}
// 析构函数:销毁信号量
~Sem()
{
int n = sem_destroy(&_sem);
(void)n;
}
private:
sem_t _sem; // 信号量变量
};
#endif
4.2环形队列封装
4.2.1单生产者单消费者
RingQueue.hpp
cpp
#ifndef __RINGQUEUE_HPP
#define __RINGQUEUE_HPP
#include <vector>
#include "Sem.hpp" //包含信号量类
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = 10)
: _cap(cap),
_rq(cap),
_consumer_step(0),
_productor_step(0),
_blank_sem(cap),
_data_sem(0)
{}
void Enqueue(T &in)
{
_blank_sem.P();
_rq[_productor_step++] = in;
_productor_step %= _cap;
_data_sem.V();
}
void Pop(T *out)
{
_data_sem.P();
*out = _rq[_consumer_step++];
_consumer_step %= _cap;
_blank_sem.V();
}
~RingQueue()
{}
private:
int _cap; // 环形队列的容量
std::vector<T> _rq; // 环形队列的底层存储
int _consumer_step; // 消费位置(消费者下次读取的下标)
int _productor_step; // 生产位置(生产者下次写入的下标)
Sem _blank_sem; // 空槽位资源计数器,生产者关心
Sem _data_sem; // 数据资源计数器,消费者关心
};
#endif // __RINGQUEUE_HPP
Main.hpp
cpp
#include "RingQueue.hpp"
#include <iostream>
#include <pthread.h>
// 生产者线程函数:负责向环形队列中生产数据
void *ProductorRoutine(void *args)
{
RingQueue *rq = static_cast<RingQueue*>(args);
int data = 1;
while(true)
{
rq->Enqueue(data);
std::cout << "生产:" << data << std::endl;
data++;
}
}
// 消费者线程函数:负责从环形队列中消费数据
void *ConsumerRoutine(void *args)
{
RingQueue *rq = static_cast<RingQueue*>(args);
int data = 0;
while(true)
{
rq->Pop(&data);
std::cout << "消费:" << data << std::endl;
}
}
int main()
{
RingQueue *rq = new RingQueue();
pthread_t c, p;
// 创建生产者线程
pthread_create(&p, nullptr, ProductorRoutine, rq);
// 创建消费者线程
pthread_create(&c, nullptr, ConsumerRoutine, rq);
// 等待线程结束
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
4.2.2多生产者多消费者
由于是多生产者和多消费者,多个生产者和多个消费者会同时修改生产位置和消费位置以及资源计数器时,就必然会产生竞争!所以我们需要在Enqueue和Pop时加上mutex互斥锁。
如果只使用一把互斥锁,那么在生产者生产并访问临界资源的同时,消费者就必须等待。但是生产者和消费者本来是可以并行的,却由于只有一把锁被迫导致变成串行。因此,我们需要在Enqueue和Pop中各加一把锁,来维持原有的并行逻辑。
使用锁时要包含上个博客中封装的Mutex.hpp:
cpp
#ifndef __RINGQUEUE_HPP
#define __RINGQUEUE_HPP
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = 10)
: _cap(cap),
_rq(cap),
_consumer_step(0),
_productor_step(0),
_blank_sem(cap),
_data_sem(0)
{}
// 多生产者入队
void Enqueue(T &in)
{
// 1. 先预定资源(P操作)
_blank_sem.P();
// 2. 再加锁保护生产位置
_pmutex.Lock();
// 3. 找位置生产
_rq[_productor_step++] = in;
_productor_step %= _cap;
// 4. 解锁
_pmutex.Unlock();
// 5. 释放数据资源(V操作)
_data_sem.V();
}
//多消费者出队
void Pop(T *out)
{
// 1. 先预定资源(P操作)
_data_sem.P();
// 2. 再加锁保护消费位置
_cmutex.Lock();
// 3. 找位置消费
*out = _rq[_consumer_step++];
_consumer_step %= _cap;
// 4. 解锁
_cmutex.Unlock();
// 5. 释放格子资源(V操作)
_blank_sem.V();
}
private:
int _cap; // 环形队列的容量
std::vector<T> _rq; // 环形队列
int _consumer_step; // 消费位置
int _productor_step; // 生产位置
Sem _blank_sem; // 格子资源计数器,生产者关心
Sem _data_sem; // 数据信号量,消费者关心
Mutex _cmutex; // 消费者互斥锁
Mutex _pmutex; // 生产者互斥锁
};
#endif
需要注意的是,无论是生产者还是消费者,在预定资源时,都要先预定资源再竞争锁,否则不仅效率很低,而且可能会导致死锁的问题!