Linux - POSIX信号量,基于环形队列的生产者消费者模型

news/2024/4/25 9:21:55/文章来源:https://blog.csdn.net/i777777777777777/article/details/129142235

信号量

在Linux下,POSIX信号量是一种线程同步机制,用于控制多个线程之间的访问顺序。POSIX信号量可以用于实现线程之间的互斥或者同步。


在之前的阻塞队列生产者消费者模型中,阻塞队列是一个共享资源,不管是生产者还是消费者,任何时刻只允许一个线程访问共享资源,所以对阻塞队列进行了加锁保护,使阻塞队列成为为临界资源。在这里,是将整个共享资源(阻塞队列)当作一个整体使用的。

信号量使用思路:一个共享资源,不当作整体使用,将其划分为若干个小的共享资源,使得不同的执行流访问不同的小共享资源,本质上,当不同执行流访问不同的小共享资源时,不需要进行加锁保护,可以并发执行。而如果两个执行流访问到了同一个小的共享资源时,再进行互斥保护共享资源。


POSIX信号量提供了两种类型的信号量:二进制信号量和计数信号量。二进制信号量只有两个取值:0和1,它用于表示资源是否被占用。计数信号量可以有多个取值,它表示可用资源的数量。

二进制信号量可以充当互斥锁的作用。因为信号量的初始值为1,p相当于加锁,v相当于解锁。

计数信号量的值通常被称为“资源计数器”,它记录了当前可用的共享资源数量。当一个线程需要使用共享资源时,它会尝试获取计数信号量,如果当前可用资源数量大于0,则线程可以获取资源并将计数信号量的值减1。如果当前可用资源数量为0,则表示共享资源已被占用,线程需要等待直到有资源可用。

POSIX信号量接口

       int sem_init(sem_t *sem, int pshared, unsigned int value); // 初始化int sem_destroy(sem_t *sem);    // 销毁int sem_wait(sem_t *sem);       // 申请信号量,P操作int sem_post(sem_t *sem);       // 释放信号量,V操作

因为信号量本质就是一个计数器,记录着当前的资源数目,其中初始化时的value值就表示初始时的资源数目。

在使用POSIX信号量时,线程可以通过调用sem_wait()函数来请求获取信号量,如果当前信号量的值大于0,则线程可以继续执行,同时信号量的值会减1;否则线程将被阻塞,等待信号量的值变为大于0。当线程释放资源时,可以通过调用sem_post()函数来释放信号量,使得其它线程可以获取资源。

基于环形队列的生产者消费者模型

1.缓冲区的改变

基于环形队列的生产者消费者模型,对比之前的基于阻塞队列的生产者消费者模型,只是缓冲区的具体数据结构变了。两个角色:生产者,消费者。三种关系:生产者与生产者:互斥。消费者与消费者:互斥。生产者与消费者:互斥与同步。

如上图,环节队列是逻辑抽象结构,底层的物理结构是数组。要想达到环形队列的效果,只需在访问数组的最后一个元素的下一个元素时将下标 %= size即可。再者,环形队列有一个判空和判满的问题,因为这里的容量是有上限的,从数据结构的角度来说,一般会有一个start和end下标,当start == end时为空,而当数据为满时,不能也将start == end标定为满,因为这样空和满的判断条件就相同了。解决方法:1. 空出一个位置,当(end + 1) % size == start时,判定为满。 2. 加一个计数器,当计数器 == size时为满,计数器 == 0时为空。

但是,因为信号量的作用,在环形队列生产消费模型中,我们不需要考虑判空判满的问题。见下文。

2. 结合生产者消费者

先考虑单生产者,单消费者的环形队列生产消费模型,有以下前提:

  1. 通过给生产者线程和消费者线程分别设定一个下标,从而标定它们当前生产和消费的下标位置。

  1. 我们不需要通过空出一个位置或者添加计数器的方式来避免队列为空和为满时生产消费线程的下标相同这样的判定条件重复。因为有信号量的存在。

  1. 因此,队列为空和为满时,生产者线程的下标一定等于消费者线程的下标。一种情况是当前缓冲区中没有数据,一种是当前缓冲区中数据已满。

目的期望:

  1. 当生产线程和消费线程指向了环形队列的同一个位置时(此时队列为空or为满),也就是访问了同一个共享资源,此时需要进行多线程互斥与同步。

  1. 当生产线程和消费线程不指向环形队列的同一个位置时,此时不需要进行互斥与同步,可以并发执行,因为没有访问同一个共享资源。

  1. 当环形队列为空时,必须让生产者先执行,消费者必须等待生产者生产完至少一个数据。这也就是第一点中的互斥与同步。

  1. 当环形队列为满时,必须让消费者先执行,生产者必须等待消费者消费完至少一个数据。这也就是第一点中的互斥与同步。

  1. 当环形队列不为空不为满时,生产线程与消费线程并发执行。

上方5点,其实345和12是一个原则。34对应1,5对应2。

3.结合信号量

信号量表征的是共享资源的数量,在环形队列生产消费模型中,设定两个信号量,分别表征数据资源数目和空间资源数目。生产者关注空间资源信号量,初始为N。消费者关注数据资源信号量,初始为0。

按照上面生产者和消费者的执行逻辑来看,每次在生产或者消费之前必须先申请信号量,申请信号量的本质是一种对资源的预定机制。若信号量大于0,则--信号量,继续执行。若为0,则阻塞等待信号量被其他线程释放。

这样一来,起始时,或当其他环形队列为空时,dataSem为0,消费者没有数据资源可以申请,故必须阻塞等待。这也就是我们期望的,当生产和消费的下标相等时,且队列为空时,必须让生产者先执行,消费者阻塞等待。

当环形队列为满时,spaceSem为0,dataSem为N,生产者没有空间资源可以申请,故必须阻塞等待。这也就是我们期望的,当生产和消费下标相等时,且队列为满时,必须让消费者先执行,生产线程必须阻塞等待。

(注意,上方两种互斥情况下,只有当另一方生产/消费完,执行完V操作之后,阻塞方才能P成功,这也就实现了互斥和同步

当环形队列不为空时,生产和消费的下标不同,此时并没有访问同一个资源,故可以并发执行。

4.代码...

ringQueue.hpp

#ifndef _RINGQUEUE_HPP_
#define _RINGQUEUE_HPP_#include <vector>
#include "sem.hpp"
#include "mutex.hpp"const int g_default_size = 5;template <class T>
class RingQueue
{
public:RingQueue(int size = g_default_size): ring_queue_(size), size_(size), space_sem_(size), data_sem_(0), p_index_(0), c_index_(0), p_mutex_(), c_mutex_(){}~RingQueue() = default;void push(const T &in){// 生产者: 环形队列的push的操作space_sem_.p(); // 空间资源的p操作p_mutex_.lock();ring_queue_[p_index_++] = in;p_index_ %= size_;p_mutex_.unlock();data_sem_.v(); // 数据资源v操作}void pop(T *out){data_sem_.p(); // 数据资源的p操作,消费者进行。若没有数据资源,则阻塞挂起消费者线程c_mutex_.lock();*out = ring_queue_[c_index_++];c_index_ %= size_;c_mutex_.unlock();space_sem_.v(); // 空间资源v操作}private:std::vector<T> ring_queue_; // 使用vector模拟环形队列int size_;                  // 环形队列的大小int p_index_;   // 生产者下标int c_index_;   // 消费者下标Sem space_sem_; // 空间资源信号量,生产者使用Sem data_sem_;  // 数据资源信号量,消费者使用Mutex p_mutex_;Mutex c_mutex_;
};// 自己写的:环形队列利用互斥锁+条件变量进行。
// template <class T>
// class RingQueue
// {
// public:
//     RingQueue(int size = g_default_size)
//     : ring_queue_(size)
//     , size_(size)
//     , c_index_(0), p_index_(0)
//     , c_mutex_(), p_mutex_()
//     {
//         pthread_cond_init(&not_empty_, nullptr);
//         pthread_cond_init(&not_full_, nullptr);
//     }
//     ~RingQueue()
//     {
//         pthread_cond_destroy(&not_empty_);
//         pthread_cond_destroy(&not_full_);
//     }
//     void push(const T &in)
//     {
//         // 生产者
//         p_mutex_.lock();  // 加锁//         while((p_index_ + 1) % size_ == c_index_)
//         {
//             // 满的
//             pthread_cond_wait(&not_full_, &p_mutex_.mtx_);
//         }
//         // 不满了
//         ring_queue_[p_index_++] = in;
//         p_index_ %= size_;
//         pthread_cond_signal(&not_empty_);//         p_mutex_.unlock(); 
//     }
//     void pop(T *out)
//     {
//         // 消费者
//         c_mutex_.lock();//         while(c_index_ == p_index_)  // 为空,没有数据资源,等待条件变量。
//         {
//             pthread_cond_wait(&not_empty_, &c_mutex_.mtx_);
//         }
//         *out = ring_queue_[c_index_++];
//         c_index_ %= size_;
//         pthread_cond_signal(&not_full_);//         c_mutex_.unlock();
//     }
// private:
//     std::vector<T> ring_queue_;  // 环形队列
//     int size_;
//     int c_index_;
//     int p_index_;
//     Mutex c_mutex_;
//     Mutex p_mutex_;
//     pthread_cond_t not_empty_;
//     pthread_cond_t not_full_;
// };#endif

testMain.cc

#include "ringQueue.hpp"
#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;// 消费者
void *consumer(void *args)
{RingQueue<int> *rq = (RingQueue<int> *)args; // 生产消费的缓冲区:环形队列while (true){sleep(1);int x;rq->pop(&x);cout << "消费:" << x << " [" << pthread_self() << "]" << endl;}pthread_exit(nullptr);
}// 生产者
void *producer(void *args)
{RingQueue<int> *rq = (RingQueue<int> *)args;while (true){int x = rand() % 100 + 1;rq->push(x);cout << "生产:" << x << " [" << pthread_self() << "]" << endl;}pthread_exit(nullptr);
}// int main()
// {
//     srand((unsigned int)time(nullptr));//     // RingQueue<int>* ring_queue = new RingQueue<int>();
//     RingQueue<int> ringQueue;
//     pthread_t c[2], p[3];     // 多生产多消费,3生产2消费//     for(int i = 0; i < 2; ++i)
//         pthread_create(c + i, nullptr, consumer, &ringQueue);
//     for(int i = 0; i < 3; ++i)
//         pthread_create(p + i, nullptr, producer, &ringQueue);//     for(int i = 0; i < 2; ++i)
//         pthread_join(c[i], nullptr);
//     for(int i = 0; i < 3; ++i)
//         pthread_join(p[i], nullptr);
//     return 0;
// }
int main()
{srand((unsigned int)time(nullptr));RingQueue<int> ringQueue;pthread_t c, p;pthread_create(&c, nullptr, consumer, &ringQueue);pthread_create(&p, nullptr, producer, &ringQueue);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}// 二元信号量充当互斥锁// int tickets = 10000;
// pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;// void *getTickets(void *args)
// {
//     Sem *sem = (Sem *)args;
//     while (true)
//     {
//         sem->p(); // 申请信号量,原子的,只有一个线程会申请成功
//         // pthread_mutex_lock(&mtx);
//         if (tickets > 0)
//         {
//             usleep(1000); // 1ms,模拟抢票过程
//             cout << "tickets : " << tickets << " [" << pthread_self() << "]" << endl;
//             tickets--;
//             sem->v();
//             // pthread_mutex_unlock(&mtx);
//         }
//         else
//         {
//             sem->v();
//             // pthread_mutex_unlock(&mtx);
//             break;
//         }
//     }
//     pthread_exit(nullptr);
// }// int main()
// {
//     pthread_t tids[4];
//     Sem sem(1);
//     for (int i = 0; i < 4; ++i)
//     {
//         pthread_create(tids + i, nullptr, getTickets, &sem);
//     }//     for (int i = 0; i < 4; ++i)
//     {
//         pthread_join(tids[i], nullptr);
//     }
//     return 0;
// }

sem.hpp

#ifndef _SEM_HPP_
#define _SEM_HPP_#include <semaphore.h>// 信号量的封装
class Sem
{
public:Sem(int value){sem_init(&sem_, 0, value);}~Sem(){sem_destroy(&sem_); }void p(){sem_wait(&sem_);}void v(){sem_post(&sem_);}
private:sem_t sem_;
};#endif

5.多生产多消费

多生产多消费时,任何一个生产者和一个消费者之间都已经通过信号量达到了应有的互斥与同步,现在需要考虑的是生产生产之间和消费消费之间的互斥关系。

生产生产之间,消费消费之间,需要保护的共享资源为下标:c_index_或者p_index_,环形队列:ring_queue_(STL:vector,不是线程安全的),故,需要在push和pop内部,进行加锁,构建临界区,保护临界资源。

因为这里生产和消费之间的互斥,已经在当下标相等时,为空或为满时,由信号量维护了,所以这里用两把锁,分别维护生产生产之间和消费消费之间。(见上方代码实现)

6.基于环形队列的生产消费模型为什么效率高,优势?

我们可以看到,多个生产之间,在对环形队列push时,内部是互斥的,也就是串型执行的。多个消费之间,在对环形队列pop时,内部是互斥的,也是串行执行的。对比阻塞队列的生产消费模型来说,改进就是,当缓冲区,环形队列不为空且不为满时,生产和消费的push和pop可以并发执行。

还是那句话,生产消费的过程并非只是pushpop的过程(放数据和拿数据的过程),还有生产者放数据之前生产数据和消费者拿数据之后处理数据的过程(这里才是最耗时间的)。多生产多消费的意义是,可以让生产数据和处理数据时多个线程并发执行,从而提高效率。 (和基于阻塞队列的一样)

7.信号量的理解

信号量的本质就是一个计数器,表示当前资源数目。计数器的意义是什么呢?

在之前使用互斥锁,条件变量的使用场景下,常规流程是:申请锁->判断临界资源是否就绪->就绪则访问,不就绪则在条件变量下等待->访问临界资源->释放锁。

而信号量这个计数器的意义就是:可以不进入临界区就得知资源情况,减少了临界区内的资源是否就绪的判断。本来,信号量就是一种资源预定机制,当P操作成功时,就代表着此时有一个资源可以供你获取/访问(比如环形队列中的空间资源/数据资源)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_72568.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql实战之日志系统:一条SQL更新语句是如何执行的

1.前言 上一篇咱们了解了MySQL 的执行过程&#xff0c;其中设计连接器、分析器、优化器、执行器和存储引擎&#xff0c;接下来我将给大家讲解一下在MySQL中一条更新语句是如何执行。我相信大家可能听公司的DBA提起过&#xff0c;可以将数据恢复到半个月内任意时间的状态&#…

Scala集合详解(第七章:集合、数组、列表、set集合、map集合、元组、队列、并行)(尚硅谷笔记)

集合第七章:集合7.1 集合简介7.1.1 不可变集合继承图7.1.2 可变集合继承图7.2 数组7.2.1 不可变数组7.2.2 可变数组7.2.3 不可变数组与可变数组的转换7.2.4 多维数组7.3 列表 List7.3.1 不可变 List7.3.2 可变 ListBuffer7.4 Set 集合7.4.1 不可变 Set7.4.2 可变 mutable.Set7.…

Android system实战 — Android R(11) 进程保活白名单

Android system实战 — Android R 进程保活白名单0. 前言1. 具体实现1.1 准备工作1.2 源码实现1.2.1 源码1.2.2 diff文件0. 前言 最近在Android R上实现一些需求&#xff0c;进行记录一下&#xff0c;关于进程保活的基础知识可以参考Android system — 进程生命周期与ADJ&#…

自动驾驶路径规划概况

文章目录前言介绍1. 路径规划在自动驾驶系统架构中的位置2. 全局路径规划的分类2.1 基础图搜索算法2.1.1 Dijkstra算法2.1.2 双向搜索算法2.1.3 Floyd算法2.2 启发式算法2.2.1 A*算法2.2.2 D*算法2.3 基于概率采样的算法2.3.1 概率路线图&#xff08;PRM&#xff09;2.3.2 快速…

蓝牙运动耳机什么牌子的好、运动蓝牙耳机排行榜推荐

近些年&#xff0c;户外运动兴起&#xff0c;运动耳机迎来爆发增长&#xff0c;拒绝运动乏味&#xff0c;追求健康运动方式&#xff0c;已经成为当下年轻人的共同诉求。跑步骑行听音乐&#xff0c;已经是运动爱好者再熟悉不过的操作&#xff0c;很多人在运动中离不开音乐的节奏…

代码随想录算法训练营第七天 | 454.四数相加II 、 383. 赎金信、15. 三数之和、18. 四数之和 、总结

打卡第七天&#xff0c;还是哈希表。 今日任务 454.四数相加II383.赎金信15.三数之和18.四数之和总结 454.四数相加II 代码随想录 class Solution { public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, ve…

【vue2每日小知识】实现directive自定义指令的封装与全局注册

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;将我们的自定义指令directive统一管理并批量注册 目录 一、directive自定义指令介绍 二…

DS期末复习卷(六)

一、选择题(30分) 1&#xff0e; 设一组权值集合W{2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6}&#xff0c;则由该权值集合构造的哈夫曼树中带权路径长度之和为&#xff08; D &#xff09;。 (A) 20 (B) 30 (C ) 40 (D) 45 W(23)*3(456)*245 2&#xff0e;执行一…

Python、Java、JavaScript、C、Go等编程语言如何实现“定时器”功能

这是CSDN平台2月推出的一个活动(活动链接为&#xff1a;CSDN 征文活动)&#xff0c;聊聊时间的话题&#xff0c;小编我也不知道有什么好聊的时间的话题&#xff0c;看了CSDN给出的部分话题上&#xff0c;有一个这样的话题&#xff0c;如何用各种编程语言实现“定时器”&#xf…

GUI可视化应用开发及Python实现

0 建议学时 4学时&#xff0c;在机房进行 1 开发环境安装及配置 1.1 编程环境 安装PyCharm-community-2019.3.3 安装PyQt5 pip install PyQt5-tools -i https://pypi.douban.com/simple pip3 install PyQt5designer -i https://pypi.douban.com/simple1.2 环境配置 选择“…

JVM13命令行

2. JVM 监控及诊断工具-命令行篇 2.1. 概述 简单命令行工具 在我们刚接触 java 学习的时候&#xff0c;大家肯定最先了解的两个命令就是 javac&#xff0c;java&#xff0c;那么除此之外&#xff0c;还有没有其他的命令可以供我们使用呢&#xff1f; 我们进入到安装 jdk 的…

【11】FreeRTOS的延时函数

目录1.延时函数-介绍2.相对延时函数-解析2.1函数prvAddCurrentTaskToDelayedList-解析2.3滴答定时器中断服务函数xPortSysTickHandler()-解析2.4函数taskSWITCH_DELAYED_LISTS() -解析3.延时函数-实验4.总结1.延时函数-介绍 函数描述vTaskDelay()相对延时xTaskDelayUntil()绝对…

CTFer成长之路之SSRF漏洞

SSRF漏洞CTF SSRF Training 题目描述: web容器中存在一个flag&#xff0c;mysql中存在一个管理员账号密码&#xff0c;其余容器中均没有特定flag mysql容器中内置 tcpdump vulnweb容器中内置一个 fpm.py 攻击脚本 docker-compose.yml version: "3" services:w…

Spring代理模式——静态代理和动态代理

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

笔记本电脑电池和充电器CE认证IEC62133测试

EC 符合性声明 - 您可以从品牌所有者或欧盟境内的商品官方进口商处获取此文件。证明您的商品经过检测符合下表所列标准的文件。您也可以从品牌所有者或欧盟境内的官方进口商处获取此文件。原品牌的笔记本电脑或手机&#xff08;如三星、苹果、戴尔、惠普等&#xff09;提供的原…

【验证码的识别】—— 点触式验证码的识别

一、前言 大家好&#xff0c;不知不觉的我来csdn已经又一周年了&#xff0c;在这一年里&#xff0c;我收获了很多东西&#xff0c;我是2022年2月22日入驻CSDN的&#xff0c;一开始只是为了方便浏览文章的&#xff0c;后来&#xff0c;我也有事没事发发文章&#xff0c;创作了1…

leetcode 1011. Capacity To Ship Packages Within D Days(D天内运送包裹的容量)

数组的每个元素代表每个货物的重量&#xff0c;注意这个货物是有先后顺序的&#xff0c;先来的要先运输&#xff0c;所以不能改变这些元素的顺序。 要days天内把这些货物全部运输出去&#xff0c;问所需船的最小载重量。 思路&#xff1a; 数组内数字顺序不能变&#xff0c;就…

Python 自动化测试必会技能板块—unittest框架

说到 Python 的单元测试框架&#xff0c;想必接触过 Python 的朋友脑袋里第一个想到的就是 unittest。的确&#xff0c;作为 Python 的标准库&#xff0c;它很优秀&#xff0c;并被广泛应用于各个项目。但其实在 Python 众多项目中&#xff0c;主流的单元测试框架远不止这一个。…

【C ++】C++入门知识(二)

C入门&#xff08;二&#xff09; 作者&#xff1a;小卢 专栏&#xff1a;《C》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 1.引用 1.1.引用的概念及应用 引用&#xff08;&&#xff09; 引用不是新定义一个变量&#xff0…

C语言格式化输出总结:%d,%c,%s,%f, %lf,%m.nd,%m.nf,%m.ns 以及sprintf函数

凡事发生必将有益于我&#xff0c;高手&#xff0c;从来都不仅仅是具备某种思维的人&#xff0c;而是那些具备良好学习习惯的人&#xff0c;成为高手&#xff0c;无他&#xff0c;手熟尔&#xff01;加油在最近的学习之中&#xff0c;对于格式化输出这个知识点&#xff0c;这里…