目录
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
初始化信号量
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: sem:指向要初始化的信号量对象的指针。 pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值 返回值: 成功时返回 0。 失败时返回 -1,并设置 errno 来指示错误类型。
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1 int sem_wait(sem_t *sem); //P()
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V()
上一章生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序 (POSIX信号量):
基于环形队列的生产消费模型
- 环形队列采用数组模拟,用模运算来模拟环状特性
模拟环形队列在我之前的文章有专门讲过,我这里就不复述了。不清楚的可以参考我LeetCode OJ循环队列(C语言)这篇文章。
- 我可以大概提一嘴:环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
- 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
RingQueue.hpp代码
#pragma once #include <iostream> #include <string> #include <vector> #include <semaphore.h> #include <pthread.h> template <typename T> class RingQueue { private: void P(sem_t &s) { sem_wait(&s); } void V(sem_t &s) { sem_post(&s); } public: RingQueue(int max_cap) : _ringqueue(max_cap), _max_cap(max_cap), _c_step(0), _p_step(0) { sem_init(&_data_sem, 0, 0); sem_init(&_space_sem, 0, max_cap); pthread_mutex_init(&_c_mutex, nullptr); pthread_mutex_init(&_p_mutex, nullptr); } void Push(const T &in) // 生产者 { P(_space_sem); pthread_mutex_lock(&_p_mutex); _ringqueue[_p_step] = in; _p_step++; _p_step %= _max_cap; pthread_mutex_unlock(&_p_mutex); V(_data_sem); } void Pop(T *out) // 消费 { P(_data_sem); pthread_mutex_lock(&_c_mutex); *out = _ringqueue[_c_step]; _c_step++; _c_step %= _max_cap; pthread_mutex_unlock(&_c_mutex); V(_space_sem); } ~RingQueue() { sem_destroy(&_data_sem); sem_destroy(&_space_sem); pthread_mutex_destroy(&_c_mutex); pthread_mutex_destroy(&_p_mutex); } private: std::vector<T> _ringqueue; int _max_cap; int _c_step; int _p_step; sem_t _data_sem; // 消费者关心 sem_t _space_sem; // 生产者关心 pthread_mutex_t _c_mutex; pthread_mutex_t _p_mutex; };
在信号量中,我们对资源是进行PV操作的,P是对资源-1,V是对资源+1.
我们先来讲成员变量的含义,首先我们对中转站还是用vector来表达,这是为了方便我们去编写,主要的原因是我们的目标是弄清楚信号量的逻辑。_max_cap还是表达我们队列的最大任务量。_c_cap表示我们的消费者消费到队列的哪一个任务了,也就是下标,同理,_p_step表达的就是我们生产者的下标。_data_sem是我们消费者关心的信号量,_space_sem是我们生产者关心的信号量。_c_mutex是我们消费者的锁,_p_mutex是我们生产者的锁。
构造函数就是正常的将我们的信号量和锁初始化一下,这里有个要点就是我们的消费者关心的信号量初始值是0,生产者的信号量初始值为_max_cap。析构函数就是将这四个变量销毁掉就行了。
接下来就是设计我们生产者生产任务和消费者消费任务的核心接口:我们先来讲生产者(Push),来到Push这个接口我们就得让资源进行P操作,让资源数量-1,然后在锁的保护下,将任务放入到队列中,将下标进行加一取模,最后我们就可以将消费者的资进行V操作,+1操作。至于为什么我们不需要将PV操作放入到锁的环境中是因为我们的PV操作本身就是原子的。消费者跟生产者的思路同理。
Task.hpp
#pragma once #include <iostream> #include <functional> using task_t = std::function<void()>; void Download() { std::cout << "我是一个下载的任务" << std::endl; }
我们的任务跟上一篇文章的是一模一样的,我就不多叙述了。
Main.cc
#include "RingQueue.hpp" #include "Task.hpp" #include <pthread.h> #include <ctime> #include <unistd.h> void *Consumer(void *args) { RingQueue<task_t> *rq = static_cast<RingQueue<task_t> *>(args); while (true) { sleep(1); task_t t; // 1.消费 rq->Pop(&t); // 2.处理数据 t(); } } void *Productor(void *args) { RingQueue<task_t> *rq = static_cast<RingQueue<task_t> *>(args); while (true) { // 1.构造数据 usleep(1000); // 2.生产 rq->Push(Download); std::cout << "Productor->Download" << std::endl; } } int main() { srand(time(nullptr) ^ getpid()); RingQueue<task_t> *rq = new RingQueue<task_t>(5); pthread_t c1, c2, p1, p2, p3; pthread_create(&c1, nullptr, Consumer, rq); pthread_create(&c2, nullptr, Consumer, rq); pthread_create(&p1, nullptr, Productor, rq); pthread_create(&p2, nullptr, Productor, rq); pthread_create(&p3, nullptr, Productor, rq); pthread_join(c1, nullptr); pthread_join(c2, nullptr); pthread_join(p1, nullptr); pthread_join(p2, nullptr); pthread_join(p3, nullptr); }
我们的源文件也是除了把两个插入任务和取出任务的接口换了一下其他没有任何变动。
运行结果:
POSIX信号量及基于它的生产者消费者模型
最新推荐文章于 2025-07-25 19:36:03 发布