【Linux】生产者消费者模型 -- RingQueue

文章目录

  • 1. 信号量
    • 1.1 信号量的引入
    • 1.2 信号量的概念
    • 1.3 信号量函数
  • 2. 二元信号量模拟实现互斥功能
  • 3. 基于环形队列的生产消费模型
    • 3.1 空间资源和数据资源
    • 3.2 生产者和消费者申请和释放资源
    • 3.3 必须遵守的两个规则
    • 3.4 代码实现
    • 3.5 信号量保护环形队列的原理

1. 信号量

1.1 信号量的引入

  • 我们将可能会被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致的问题。
  • 当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。
  • 但实际我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致的问题。

如果要让执行流同时访问临界资源的不同区域的话,就需要引入信号量了。

1.2 信号量的概念

信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度地对临界资源进行管理。

每个执行流在进入临界区之前先申请信号量,申请成功就有了操作临界资源的权限,当操作完毕后就应该释放信号量。
在这里插入图片描述
信号量的PV操作

  • P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作本质就是让计数器减一。
  • V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目应该加一,因此V操作的本质就是让计数器加一。

1.3 信号量函数

信号量的初始化函数

在这里插入图片描述
参数说明:

  • sem:需要初始化的信号量
  • pshared:传入0值表示线程间共享,传入非0值表示进程间共享
  • value:信号量的初识值(计数器的初始值)

返回值说明:

  • 初始化信号量返回0,失败返回-1

注意:POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突地访问共享资源的目的,但是POSIX信号量可以用于线程间同步。

信号量的销毁函数

在这里插入图片描述
参数说明:

  • sem:需要销毁的信号量

返回值说明:

  • 销毁信号量成功返回0,失败返回-1。

等待信号量(申请信号量)

在这里插入图片描述
参数说明:

  • sem:需要等待的信号量

返回值说明:

  • 等待信号量成功返回0,信号量的值减一
  • 等待信号量失败返回-1,信号量的值保持不变

发布信号量(释放信号量)

在这里插入图片描述
参数说明:

  • sem:需要发布的信号量

返回值说明:

  • 发布信号量成功返回0,信号量的值加一
  • 发布信号量失败返回-1,信号量的值保持不变

2. 二元信号量模拟实现互斥功能

信号量本质是一个计数器,如果将信号量的初始值设置为1,那么此时该信号量叫做二元信号量

信号量的初识值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁。

例如,下面我们实现一个多线程抢票系统,其中我们用二元信号量模拟实现多线程互斥。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

class Sem
{
public:
    Sem(int num)
    {
        sem_init(&_sem, 0, num);
    }

    ~Sem()
    {
        sem_destroy(&_sem);
    }

    void P()
    {
        sem_wait(&_sem);
    }

    void V()
    {
        sem_post(&_sem);
    }

private:
    sem_t _sem;
};

Sem sem(1); // 二元信号量
int tickets = 1000;

void* ticketGet(void* arg)
{
    std::string name = (char*)arg;
    while (true)
    {
        sem.P();
        if (tickets > 0)
        {
            usleep(1000);
            std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
            sem.V();
        }
        else
        {
            sem.V();
            break;
        }
    }
    std::cout << name << " quit..." << std::endl;
    pthread_exit(nullptr);
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, ticketGet, (void*)"thread 1");
    pthread_create(&t2, nullptr, ticketGet, (void*)"thread 2");
    pthread_create(&t3, nullptr, ticketGet, (void*)"thread 3");
    pthread_create(&t4, nullptr, ticketGet, (void*)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    return 0;
}

运行结果如下:
在这里插入图片描述

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

在这里插入图片描述

3.1 空间资源和数据资源

生产者关注的是空间资源,消费者关注的是数据资源

对于生产者和消费者来说,它们关注的资源是不同的:

  • 生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产
  • 消费者关注的是环形队列中是否有数据(data),只要有数据就可以进行消费。

blank_sem和data_sem的初始值设置

现在我们用信号量来描述环形队列中的空间资源(blank_sem)和数据资源(data_sem),在我们初识化信号量时给它们设置的初始值是不同的;

  • blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
  • data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据。

3.2 生产者和消费者申请和释放资源

生产者申请空间资源,释放数据资源

对于生产者来说,生产者每次生产数据前都需要先申请blank_sem:

  • 如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
  • 如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。

当生产者生产完数据之后,应该释放data_sem:

  • 虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据之后,应该对data_sem进行V操作而不是blank_sem
  • 生产者在生产数据前申请到的是blank位置,当生产者生产完数据之后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而是data位置。
  • 当生产者生产完数据之后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。

消费者申请数据资源,释放空间资源

对于消费者来说,消费者每次消费数据前都需要先申请data_sem:

  • 如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作
  • 如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。

当消费者消费完数据之后,应该释放blank_sem:

  • 虽然消费者进行消费前是对data_sem进行的P操作,但是当消费者消费完数据之后,应该对blank_sem进行V操作而不是data_sem
  • 消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数,我们应该将该位置算作blank位置,而不是data位置。
  • 当消费者消费完数据之后,意味着环形队列中多了一个blank位置,因此我们应该对blank_sem进行V操作。

3.3 必须遵守的两个规则

第一个规则:生产者和消费者不能对同一个位置进行访问

生产者和消费者在访问环形队列时:

  • 如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对着一块临界资源进行了访问,这当然是不允许的。
  • 而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致的问题。

第二个规则:无论是生产者还是消费者,都不能超过对方一圈以上

  • 生产者从消费者的位置开始一直按顺时针的方向进行生产,如果生产者生产的速度比消费者消费的数据快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该继续生产了,因为在生产就会覆盖还未被消费的数据。
  • 同理,消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费的速度比生产者生产的速度还快,那么当消费者绕着生产者消费量一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到之前保存的数据了。

3.4 代码实现

ringQeueu的代码实现如下:

#pragma once

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>

#define NUM 8

template <class T>
class ringQueue
{
private:
    // P操作
    void P(sem_t& s)
    {
        sem_wait(&s);
    }

    // V操作
    void V(sem_t& s)
    {
        sem_post(&s);
    }

public:
    ringQueue(int cap = NUM)
        : _cap(cap), _p_pos(0), _c_pos(0)
    {
        _q.resize(_cap);
        sem_init(&_blank_sem, 0, _cap); // blank_sem的初始值为环形队列的容量
        sem_init(&_data_sem, 0, 0); // data_sem的初始值设置为0
    }

    ~ringQueue()
    {
        sem_destroy(&_blank_sem);
        sem_destroy(&_data_sem);
    }

    void push(const T& data)
    {
        P(_blank_sem); // 申请blank信号量
        _q[_p_pos] = data; // 生产数据
        V(_data_sem); // 释放data信号量

        // 更新下一次生产的位置
        _p_pos++;
        _p_pos %= _cap;
    }

    void pop(T& out)
    {
        P(_data_sem); // 申请data信号量
        out = _q[_c_pos]; // 消费数据 
        V(_blank_sem); // 释放blank信号量

        // 更新下一次消费的位置
        _c_pos++;
        _c_pos %= _cap; 
    }

private:
    std::vector<T> _q; // 环形队列
    int _cap; // 环形队列的容量上限
    int _p_pos; // 生产位置
    int _c_pos; // 消费位置
    sem_t _blank_sem; // 描述数据资源
    sem_t _data_sem; // 描述数据资源
};

为了方便理解,我们实现单生产者单消费者的模型。在主函数中创建一个生产者线程和一个消费者线程,生产者线程不断将数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。

#include "ringQueue.hpp"

void* productor(void* arg)
{
    ringQueue<int>* rq = (ringQueue<int>*)arg;
    while (1) 
    {
        sleep(1);
        int data = rand() % 100 + 1;
        rq->push(data);
        std::cout << "productor: " << data << std::endl;
    }
}

void* consumer(void* arg)
{
    ringQueue<int>* rq = (ringQueue<int>*)arg;
    while (1)
    {
        int data = 0;
        rq->pop(data);
        std::cout << "consumer: " << data << std::endl;
    }
}

int main()
{
    srand((unsigned int)time(nullptr));
    pthread_t p, c;
    ringQueue<int>* rq = new ringQueue<int>();
    pthread_create(&p, nullptr, productor, rq);
    pthread_create(&c, nullptr, consumer, rq);

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);

    delete rq;

    return 0;
}

运行结果如下:
在这里插入图片描述
在我们自己所写的代码中,我们虽然只让生产者一秒生产一次,而没让消费者的消费速度受限制,但是它们最终的步调是一致的,这正是信号量的作用所在。

3.5 信号量保护环形队列的原理

在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。

因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一位置:

环形队列为空时
环形队列为满时

但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:

  • 当环形队列为空时,消费者一定不能进行消费,因为此时数据资源为0。
  • 当环形队列为满时,生产者一定不能进行生产,因为此时空间资源为0。

也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一位置,因此大部分情况下该环形队列可以让生产者和消费者并发地执行。

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

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

相关文章

Java 串口通讯 Demo

为什么写这篇文章 之前职业生涯中遇到的都是通过tcp协议与其他设备进行通讯&#xff0c;而这个是通过串口与其他设备进行通讯&#xff0c;意识到这里是承重板的连接&#xff0c;但实际上比如拉力、压力等模拟信号转换成数字信号的设备应该是有相当一大部分是通过这种方式通讯的…

6.溢出的文字省略号显示

6.1单行文本溢出显示省略号 必须满足三个条件 /*1. 先强制一行内显示文本*/ white-space: nowrap; &#xff08; 默认 normal 自动换行&#xff09; /*2. 超出的部分隐藏*/ overflow: hidden; /*3. 文字用省略号替代超出的部分*/ text-overflow: ellipsis;【示例代码】 <…

Redis学习(三)持久化机制、分布式缓存、多级缓存、Redis实战经验

文章目录 分布式缓存Redis持久化RDB持久化AOF持久化 Redis主从Redis数据同步原理全量同步增量同步 Redis哨兵哨兵的作用和原理sentinel&#xff08;哨兵&#xff09;的三个作用是什么&#xff1f;sentinel如何判断一个Redis实例是否健康&#xff1f;master出现故障后&#xff0…

Windows下PyTorch深度学习环境配置(GPU)

一&#xff1a;下载Anaconda &#xff08;路径最好全英文&#xff09; &#xff08;下载好后&#xff0c;可以创建其他虚拟环境&#xff0c;因为是自己学习&#xff0c;所以先不放步骤&#xff0c;有需要者可以参考B站up我是土堆的视频&#xff09; 二&#xff1a;利用 conda…

本地生活直播,和电商直播有什么不一样?

直播正在成为零售业的标配&#xff0c;当下最新的一条赛道是“本地生活直播”。 &#xff08;商家开始在美团等平台进行本地生活直播。摄影&#xff1a;李崧稷&#xff09; 今年618&#xff0c;在老牌电商平台拉着无数网店&#xff0c;拼尽全力想要堆高销量的时候&#xff0c;一…

《TCP IP网络编程》第六章

《TCP IP网络编程》第六章&#xff1a;基于 UDP 的服务端/客户端 UDP 套接字的特点&#xff1a; 通过寄信来说明 UDP 的工作原理&#xff0c;这是讲解 UDP 时使用的传统示例&#xff0c;它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址&#xff0c;之后…

pytorch+CRNN实现

最近接触了一个仪表盘识别的项目&#xff0c;简单调研以后发现可以用CRNN来做。但是手边缺少仪表盘数据集&#xff0c;就先用ICDAR2013试了一下。 结果遇到了一系列坑。为了不使读者和自己在以后的日子继续遭罪。我把正确的代码发到下面了。 1&#xff09;超参数请不要调整&am…

抖音新号起号正确方法,如何操作?

抖音上有着越来越多的卖家注册账号&#xff0c;但刚开始在注册账号后&#xff0c;新号是没有什么流量的&#xff0c;所以想要获得更多的流量的话&#xff0c;在刚开始进行起号的时候就需要按照以下方式进行&#xff0c;下面就一起了解清楚。 第一个找对标内容&#xff0c;抖音…

04 QT坐标系

在QT中默认左上角为原点&#xff0c;即&#xff08;0,0&#xff09;点。x轴右侧为正方向&#xff0c;y轴以下侧为正方向

解锁编程世界的魔法密码:探索算法的奥秘与应用

一个程序员一生中可能会邂逅各种各样的算法&#xff0c;但总有那么几种&#xff0c;是作为一个程序员一定会遇见且大概率需要掌握的算法。今天就来聊聊这些十分重要的“必抓&#xff01;”算法吧~* 一&#xff1a;引言 算法是解决问题和优化程序性能的核心&#xff0c;它是一…

Notepad++ 配置python虚拟环境(Anaconda)

Notepad配置python运行环境步骤&#xff1a; 打开Notepad ->”运行”菜单->”运行”按钮在弹出的窗口内输入以下命令&#xff1a; 我的conda中存在虚拟环境 (1) base (2) pytorch_gpu 添加base环境至Notepad中 cmd /k chdir /d $(CURRENT_DIRECTORY) & call cond…

LCD—STM32液晶显示(2.使用FSMC模拟8080时序)

目录 使用STM32的FSMC模拟8080接口时序 FSMC简介 FSMC NOR/PSRAM中的模式B时序图 用FSMC模拟8080时序 重点&#xff1a;HADDR内部地址与FSMC地址信号线的转换&#xff08;实现地址对齐&#xff09; 使用STM32的FSMC模拟8080接口时序 ILI9341的8080通讯接口时序可以由STM32使…

Java项目查询统计表中各状态数量

框架&#xff1a;SpringBoot&#xff0c;Mybatis&#xff1b;数据库&#xff1a;MySQL 表中设计2个状态字段&#xff0c;每个字段有3种状态&#xff0c;统计这6个状态各自的数量 sql查询语句及结果如图 SQL&#xff1a; SELECT SUM(CASE WHEN A0 THEN 1 ELSE 0 END) AS A0…

准备WebUI自动化测试面试?这30个问题你必须掌握(一)

本文共有8600字&#xff0c;包含了前十五个问题&#xff0c;如需要后十五个问题&#xff0c;可查看文末链接~ 1. 什么是WebUI自动化测试&#xff1f; WebUI自动化测试是指使用自动化测试工具和技术来模拟用户在Web用户界面&#xff08;UI&#xff09;上执行操作&#xff0c;并…

(转载)BP神经网络的非线性系统建模(matlab实现)

本博客的完整代码获取&#xff1a; https://www.mathworks.com/academia/books/book106283.html 1案例背景 在工程应用中经常会遇到一些复杂的非线性系统,这些系统状态方程复杂,难以用数学方法准确建模。在这种情况下,可以建立BP神经网络表达这些非线性系统。方法把未知系统看…

深度学习环境安装|PyCharm,Anaconda,PyTorch,CUDA,cuDNN等

本文参考了许多优秀博主的博客&#xff0c;大部分安装步骤可在其他博客中找到&#xff0c;鉴于我本人第一次安装后&#xff0c;时隔半年&#xff0c;我忘记了当时安装的许多细节和版本信息&#xff0c;所以再一次报错时&#xff0c;重装花费了大量时间。因此&#xff0c;我觉得…

【JAVA】方法的使用:方法语法、方法调用、方法重载、递归练习

&#x1f349;内容专栏&#xff1a;【JAVA从0到入门】 &#x1f349;本文脉络&#xff1a;JAVA方法的使用&#xff0c;递归练习 &#x1f349;本文作者&#xff1a;Melon_西西 &#x1f349;发布时间 &#xff1a;2023.7.19 目录 1. 什么是方法(method) 2 方法定义 2.1 方法…

自洽性改善语言模型中的思维链推理7.13、7.14

自洽性改善语言模型中的思维链推理 摘要介绍对多样化路径的自洽实验实验设置主要结果当CoT影响效率时候&#xff0c;SC会有所帮助与现有方法进行比较附加研究 相关工作总结 原文&#xff1a; 摘要 本篇论文提出了一种新的编码策略——自洽性&#xff0c;来替换思维链中使用的…

echarts x轴文字过长 文字换行显示

xAxis: {type: "category",data: [四美休闲娱乐文化场馆, 资讯, 大咖分享],axisLabel: {show: true,fontSize: 10,interval: 0,color: "#CAE8EA",formatter: function (params) {var newParamsName "";var paramsNameNumber params.length;var…

论文笔记--OpenPrompt: An Open-source Framework for Prompt-learning

论文笔记--OpenPrompt: An Open-source Framework for Prompt-learning 1. 文章简介2. 文章概括3 文章重点技术4. 文章亮点5. 原文传送门 1. 文章简介 标题&#xff1a;OpenPrompt: An Open-source Framework for Prompt-learning作者&#xff1a;Ning Ding, Shengding Hu, We…