欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对Linux OS感兴趣的朋友,让我们一起进步!
声明:本文章还是基于生产者消费者模型特点,实现的第二种方法,原则还是遵循 321 \color{OrangeRed}321 321原则。
POSIX信号量是POSIX标准(Portable Operating System Interface)定义的一种同步机制,用于进程间或线程间的同步和互斥,防止多个线程或进程同时访问共享资源导致的数据竞争问题。
【信号量】本质是一个计数器,表示临界资源的多少。
sem_init 是 POSIX 信号量(semaphore)中的一个函数,用于初始化一个未命名的信号量。信号量常用于多线程编程中,用来实现同步,控制对共享资源的访问。
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:指向 sem_t 类型的信号量变量的指针。
如果为 0,表示这个信号量在进程内部共享,适用于线程间同步。
如果非 0,表示这个信号量用于多个进程间共享(需要放在共享内存中)。
value:信号量的初始值,通常表示资源的数量或允许访问的线程数。
成功返回 0。
失败返回 -1,且设置 errno 表示错误原因。
sem_wait 是 POSIX 信号量(semaphore)操作中的一个函数,用于对信号量执行等待(P操作),即尝试减少信号量的值,若信号量值为0,调用线程将被阻塞,直到信号量大于0。
int sem_wait(sem_t *sem);
sem:指向已经初始化的信号量变量的指针。
成功返回 0。如果出错,返回 -1,且设置 errno,常见错误如段错误(EINVAL)信号量无效等。
函数解释:该函数会尝试对信号量的值进行减1,如果信号量的值大于0,则直接减1立即返回,线程继续执行后续代码;如果信号量的值小于0,则调用线程进行阻塞,直至有其他线程对该信号量执行V操作,使其值大于0。
sem_post 是 POSIX 信号量(semaphore)操作中的一个函数,用于释放信号量(V操作),即将信号量的值增加1,表示资源可用数量增加,可能会唤醒一个等待该信号量的阻塞线程。
int sem_post(sem_t *sem);
sem:指向已初始化的信号量对象的指针。
成功返回0。失败返回-1,同时设置 errno,比如信号量对象无效(EINVAL)等。
函数解释:该函数会释放资源,并使信号量的值+1,如果有线程因为调用 sem_wait 而在该信号量上阻塞,系统会唤醒其中一个线程,使其能够继续执行。因为信号量的值不小于0,操作系统就会唤醒正在等待的线程。
sem_destroy 是 POSIX 信号量(semaphore)操作中的一个函数,用于销毁已经初始化的信号量,释放其占用的资源。
int sem_destroy(sem_t *sem);
sem:指向一个已经初始化的信号量对象的指针。
成功返回 0。失败返回 -1,且设置 errno,比如传入了无效的信号量指针(EINVAL)。
函数解释:sem_destroy 用于销毁信号量,释放系统资源。注意:释放该资源后,不能对该信号量进行初始化,P或V操作。
冷知识:环形队列可以用数组模拟,通过模运算来模拟。
【生产者消费者模型】的交易场所并非只能是阻塞队列,还可以是环形队列,这里的环形队列并非指的是传统意义上的 “队列”,而是用数组通过模运算来模拟的。
思考一下:环形队列如何判断为满,为空???
可以使用 rear == front来判断两个状态吗?答案是不行,解释:假设数组的容量为5,插入5个数据后,front重新回到了rear的位置,这个位置既表示空,又表示满吗,明显矛盾了。所以该策略行不通。得换想法。
策略1:多开一个空间,rear,front位于同一位置时,表示队列为空;进行插入数据时,当 front 的下一个位置是rear时,表示队列为满。
策略2:可以使用一个计数器count,当count ==0 表示队列为空,当 count == 队列的容量时,表示队列为满。
众所周知:信号量本身就是一个计数器,所以选择策略2合适。
运转模式:
思考一下:需要什么成员变量
伪代码如下:
template <typename T>
class RingQueue
{
private:
std::vector<T> _rq;
int _cap;
// 消费者
Sem _blank_sem;
int _p_step;
// 生产者
Sem _data_sem;
int _c_step;
Mutex _cmutex;
Mutex _pmutex;
};
为了支持处理任意数据,咱们设计成模版。
思考一下:为什么需要两把锁,一把锁可以吗???
答:一把锁可以,但性能显著降低且效率低下,举个例子:当消费者去超市消费商品时,生产者不能生产商品吗,答案是否定的,因为一把锁的存在,生产者必须等消费者消费完数据后,生产者才能申请锁,往超市运输产品,明显效率低下;而两把锁,消费者消费商品的同时,生产者也可以将生产的产品运输至该超市,效率明显大大的提高,为什么可以呢,因为是两把锁,消费者不需要等生产者的锁释放,只需等相同工产释放锁就可以生产了。
现在就需要对成员变量进行初始化了,构造函数就可以完成该工作
伪代码如下:
static const int gcap = 5;
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
: _cap(cap),
_rq(cap), //_rq = std::vector(cap); // 初始化列表写法更高效
_blank_sem(cap),
_p_step(0),
_data_sem(0),
_c_step(0)
{
}
};
思考一下:为什么_blank_sem为cap,而_data_sem为0???
刚开始时,生产者没有生产数据,即数据位置为0,因为任意位置没有数据,即空位置为数组容量的大小。
析构函数完成资源的清理,毕竟做任何事有始有终。
伪代码:
template <typename T>
class RingQueue
{
public:
~RingQueue() {}
};
思考一下:生产者如何往环形队列中插入数据???
先申请信号量,在哪申请 -> 生产者关注的是空位置,即__blank_sem执行P操作,生产数据将_blank_sem的值-1,将数据放入指定位置的同时维持环形特性,最后释放信号量。
伪代码如下:
template <typename T>
class RingQueue
{
public:
void Equeue(const T &in)
{
// 1. 申请信号量,将信号量减1
_blank_sem.P();
{
LockGuard lockguard(_cmutex);//防止其它生产者生产,造成数据不一致问题
// 2. 生产
_rq[_p_step] = in;
// 3. 更新下标
++_p_step;
// 4. 维持环形特性
_p_step %= _cap;
}
_data_sem.V(); // 将数据个数+1
}
};
思考一下:消费者如何消费数据呢???
先申请信号量,在哪申请 -> 消费者关注的是有数据位置,即__data_sem执行P操作,消费数据将_data_sem的值-1,将数据取出的同时维持环形特性,最后释放信号量。
伪代码如下:
template <typename T>
class RingQueue
{
public:
void Pop(T *out)
{
// 消费者
// 1. 申请信号量,数据信号量
_data_sem.P(); // 如果没有数据,线程将会在此处被阻塞
{
LockGuard lockguard(_pmutex);
// 2. 消费
*out = _rq[_c_step];
// 3. 更新下标
++_c_step;
// 4. 同上
_c_step %= _cap;
}
_blank_sem.V();
}
};
思考一下:这里好像没有用到跟阻塞队列一样使用while循环判断???
因为P,V操作是原子的,当条件不满足时,会进行自动阻塞,里面就有判断的逻辑。
最后一个问题:可以把加锁放在申请信号量前吗???
没必要,举个例子,当你看电影时,是直接去排队还是先买票(这个买票就是先申请信号量),肯定是先买票,然后在排队,你把票全售出了,然后再来排队,当你买的票后,在申请进入电影院看电影,这不大大提高运行效率,假如电影院只有100张票,比如前一种方案100万人全来排队,然后直接买票,导致100万-100的人买不到票只能干等着,什么都做不了;第二种方案100万人中只有100人可以买到票,然后申请进入电影院,而100万-100剩余的人可以去其它的电影院看其他电影,明显提高效率。
需要改代码吗???不需要,因为锁的存在维持了消费者与消费者之间,生产者与生产者之间的互斥关系。
Sem.hpp
#pragma once
#include
#include
#include
namespace SemModule
{
const int defaultvalue = 1;
class Sem
{
public:
Sem(unsigned int sem_value = defaultvalue)
{
sem_init(&_sem,0,sem_value);
}
void P()
{
//核心功能是原子性地减少信号量的值,并根据信号量的状态决定是否阻塞当前线程/进程。
int n = sem_wait(&_sem);//-1原子的
(void)n;
}
void V()
{
//功能是原子性地增加信号量的值,并可能唤醒因等待该信号量而阻塞的线程/进程。
int n = sem_post(&_sem);//+1原子的
(void)n;
}
~Sem()
{
sem_destroy(&_sem);
}
private:
sem_t _sem;
};
}
RingQueue.hpp
#pragma once
#include
#include
#include "Sem.hpp"
#include "Mutex.hpp"
using namespace SemModule;
using namespace MutexModule;
static const int gcap = 5;
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
: _cap(cap),
_rq(cap), //_rq = std::vector(cap); // 初始化列表写法更高效
_blank_sem(cap),
_p_step(0),
_data_sem(0),
_c_step(0)
{
}
void Equeue(const T &in)
{
// 1. 申请信号量,将信号量减1
_blank_sem.P();
{
LockGuard lockguard(_cmutex);
// 2. 生产
_rq[_p_step] = in;
// 3. 更新下标
++_p_step;
// 4. 维持环形特性
_p_step %= _cap;
}
_data_sem.V(); // 将数据个数+1
}
void Pop(T *out)
{
// 消费者
// 1. 申请信号量,数据信号量
_data_sem.P(); // 如果没有数据,线程将会在此处被阻塞
{
LockGuard lockguard(_pmutex);
// 2. 消费
*out = _rq[_c_step];
// 3. 更新下标
++_c_step;
// 4. 同上
_c_step %= _cap;
}
_blank_sem.V();
}
~RingQueue() {}
private:
std::vector<T> _rq;
int _cap;
// 消费者
Sem _blank_sem;
int _p_step;
// 生产者
Sem _data_sem;
int _c_step;
Mutex _cmutex;
Mutex _pmutex;
};
Mutex.hpp
#pragma once
#include
#include
#include
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
if (n == 0)
{
//std::cout << "加锁成功" << std::endl;
}
else
{
//std::cout << "加锁失败" << strerror(n) << std::endl;
}
}
void UnLock()
{
int n = pthread_mutex_unlock(&_mutex);
if (n == 0)
{
//std::cout << "解锁成功" << std::endl;
}
else
{
//std::cout << "解锁失败" << strerror(n) << std::endl;
}
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.UnLock();
}
private:
Mutex &_mutex;
};
};
测试代码:Main.cc
#include
#include
#include
#include "RingQueue.hpp"
struct threaddata
{
RingQueue<int> *rq;
std::string name;
};
int data =1;
void *consumer(void *args)
{
threaddata *td = static_cast<threaddata*>(args);
while (true)
{
sleep(3);
// 1. 消费任务
int t = 0;
td->rq->Pop(&t);
// 2. 处理任务 -- 处理任务的时候,这个任务,已经被拿到线程的上下文中了,不属于队列了
std::cout << td->name << " 消费者拿到了一个数据: " << t << std::endl;
}
}
void *productor(void *args)
{
threaddata *td = static_cast<threaddata*>(args);
while (true)
{
sleep(1);
// 1. 获得任务
std::cout << td->name << " 生产了一个任务: " << data << std::endl;
// 2. 生产任务
td->rq->Equeue(data);
data++;
}
}
int main()
{
RingQueue<int> *rq = new RingQueue<int>();
pthread_t c[2], p[3];
threaddata *td = new threaddata();
td->name = "cthread-1";
td->rq = rq;
pthread_create(c, nullptr, consumer, td);
threaddata *td2 = new threaddata();
td2->name = "cthread-2";
td2->rq = rq;
pthread_create(c + 1, nullptr, consumer, td2);
threaddata *td3 = new threaddata();
td3->name = "pthread-3";
td3->rq = rq;
pthread_create(p, nullptr, productor, td3);
threaddata *td4 = new threaddata();
td4->name = "pthread-4";
td4->rq = rq;
pthread_create(p + 1, nullptr, productor, td4);
threaddata *td5 = new threaddata();
td5->name = "pthread-5";
td5->rq = rq;
pthread_create(p + 2, nullptr, productor, td5);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
return 0;
本文介绍了基于POSIX信号量与环形队列实现的生产者-消费者模型。核心内容包括:1)POSIX信号量机制(sem_init/sem_wait/sem_post等接口)及线程/进程间同步原理;2)环形队列实现细节,通过模运算解决空满判断难题,采用双信号量(空白位计数_blank_sem与数据计数_data_sem)控制生产消费节奏;3)双锁机制(_cmutex/_pmutex)设计,通过分离生产者与消费者临界区提升并发性能,对比单锁方案的效率优势;4)给出模板化环形队列的C++实现代码及多线程测试案例,验证模型在并发场景下的稳定性。