Linux小黑板(14):基于环形队列的生成消费者模型

"多少人都,生来纯洁完美,心底从不染漆黑。" 


        我们先来瞅瞅我们之前基于阻塞队列的生产消费者模型代码。

    void Push(const T& in)
    {
        // 生产任务
        pthread_mutex_lock(&_mutex);
        while(is_full()){
            pthread_cond_wait(&_pcond,&_mutex);
        }

        // 走到这里说明 正常生产
        _q.push(in);

        // 生成后 唤醒消费者 可以来消费了
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
    }

        这有什么问题嘛? 唔,也许没什么问题。难道是存在并发安全? 当然不是!

         每一个线程进入在操作临界资源的时候,这个临界资源必须是满足一定条件的。可是,公共资源是否 满足生产、消费条件。

        即我们观察该阻塞队列是否为满或者是否为空?我们事先(线程加锁前)是无法得知的。 反而,当我们要进行条件检测时,需要先加锁,再检测(本质也是在访问临界资源),再操作,再解锁

        由此,如果有一种变量或者方法,能够支持我们事先就知道临界资源的使用情况(即条件是否满足),而不是加锁,检测,等待释放锁……,免去这些对性能造成影响的不必要步骤,那就太好了。

        本篇的主题,就是为了来谈谈一个维护同步机制的技术——信号量。

       ---前言


 

一、信号量

        什么是信号量呢?这个就很模糊了。

        信号量(Semaphore),是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

                                                                                                                                   取自这里

        以下是我的操作系统教材的解释:
信号量是一种特殊的变量,它用来表示系统中资源的使用情况。

        似乎第二段的描述更简化,于是乎有那么一丢丢得理解了,但是这太宏观了。在Llinux中有两套信号量标准,分别是SystemV信号量 与 POSIX信号量。它们之间的区别很大部分在于:

System V信号量 通常在于进程间通信,维护进程同步

POSIX 信号量常常用作维护线程同步。 

(1)POSIX信号量是什么?

        这里也就先不卖关子了。

①信号量的本质是一个计数器;        --->   表示系统资源的使用情况,衡量临界资源数量的多少

②对于信号量的操作叫 P() V()操作。

如何理解信号量?

        只要拥有信号量,那就决定了,你在未来一定拥有临界资源的一部分。申请信号量的本质,就是对临界资源小块中的 "预定" 机制。就像我们线上购买影院票,不是我到线下坐到影院坐凳上,才表示我在一段时间内,拥有对该凳子的使用权,而是向售票app预定 "座位号" ,一旦成功,就表名你一定能在未来,拥有对该临界资源的使用权。

         

如何理解信号量P、V操作?

        线程需要访问临界资源中的某一个区域,首先需要先 申请信号量 !要先申请信号量,那么所有人都需要看到这个信号量!那么这个信号量本身一定是一个共享资源。

        我们又说,信号量的本质就是一个计数器。那么它必然伴随着 递减(--) 或者 递增(++)的操作。

sem--: 说明该临界资源变少,说明已经被申请拿走一部份了 ---> "申请资源"     ---> P

sem++: 说明该临界资源增多了,说明一部分资源已经被归还了 ---> "释放资源"   ---> V

        它是共享资源,它需不需要被保护呢?当然需要,因此 对信号量的++、--的操作一定是原子的。 信号量操作核心又称作是:  PV原语

(2)POSIX信号量相关操作

初始化和销毁:

       #include <semaphore.h>
       int sem_init(sem_t *sem, int pshared, unsigned int value);

       #include <semaphore.h>
       int sem_destroy(sem_t *sem);

//Return Val
sem_init() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

sem_destroy() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

P(申请)V(释放)信号量:

       #include <semaphore.h>
       int sem_wait(sem_t *sem);

       #include <semaphore.h>
       int sem_post(sem_t *sem);

//Return Val
sem_wait() functions return 0 on success; on error, the value of the semaphore is left 
unchanged, -1 is returned, and errno is set  to  indicate the error.

sem_post() returns 0 on success; on error, the value of the semaphore is left unchanged, -1 is returned, and errno is set to indicate the error.

        函数的使用没啥可讲的。我们借此,可以来实现基于环形队列的生产消费者模型了。

二、环形队列的生产消费者模型

        什么是环形队列呢?这是一种常用的数据结构。在物理结构上,环形队列和我们平常所见的vector 、数组相差无几,都是一段连续开辟的数组。但是,在逻辑结构上,通过控制队列的下标,实现一种环形的插入结构,就叫做环形队列。

        而对于实现环形队列很重要的设计就在于,判空和判满的条件。多说无益,我们来看看。

(1)为什么会选择使用环形队列呢? 

        同样,使用环形队列实现生产者消费者模型,仍然需要遵守"321"原则。而怎样维护这个原则呢?无非只需要我们遵守以下的原则。

1. Consumer不能超过Producer (不可能我都没生产你跑到我前面进行消费)

2. Producer 不能超过 Consumer一圈 (数据将会被覆盖,这是不能容忍的事情)

3. 当我们站在一起的时候,谁才能访问到该临界资源。

         这就完了吗?你可只是说了这个环形队列是用来干嘛的,怎么使用的,可是,你还没有说为什么要选择环形队列,为什么之前的阻塞队列实现的生产者消费者模型不行~

 

(2)基于环形队列的生产消费者模型实现

环形队列:

template<class T>
class RunQueue
{
public:
    RunQueue(int cap = default_cap) : _queue(cap), _cap(cap)
    {
        // 生产者而言 当没有生成资源是 剩余空间为 _cap
        int n = sem_init(&_SpaceNum, 0, _cap);
        assert(n == 0);
        (void)n;
        // 对于消费者而言 首次入队列是 没有可消费的资源
        n = sem_init(&_DataNum, 0, 0);
        assert(n == 0);
        (void)n;
    }

    ~RunQueue()
    {
        sem_destroy(&_SpaceNum);
        sem_destroy(&_DataNum);
    }

private:
    std::vector<T> _queue;
    size_t _cap;

    // 生产者: 剩余空间资源 消费者: 空间内容
    sem_t _SpaceNum;
    sem_t _DataNum;

    // 生产者生成位置处 消费者消费位置处
    size_t _ProducerStep;
    size_t _ConsumerStep;
}

  

队列的P、V操作:

template <class T>
class RunQueue
{
    // ....
    void P(sem_t &sem)
    {
        // 申请资源
        // 如果信号量--到0 那么就会阻塞
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t &sem)
    {
        // 释放资源
        // 信号量++ 如果该信号量>0 则会唤醒处在sem_wait中的一个线程
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }
    // ....
}

Push\Pop:

    void Push(const T &in)
    {
        // 生产者进来申请信号量
        P(_SpaceNum);
        // 走到这里说明 申请成功
        _queue[_ProducerStep++] = in;
        _ProducerStep %= _cap;
        // 这里应该释放什么资源呢?
        // 你生产者生产了资源,在队列中用了嘛? 当然没有!
        // 反而你生产资源 在消费者看来 说明可以进行消费了
        V(_DataNum);
    }

    void Pop(T &out)
    {
        P(_DataNum);
        out = _queue[_ConsumerStep++];
        _ConsumerStep %= _cap;
        // 同上
        V(_SpaceNum);
    }

 

(3)优化改进

        但其实,上述的实现只是针对了单线程消费、单线程生产完成的同步互斥。如果面对多线程消费、多线程生产呢?生产者与生产者,消费者与消费者的互斥关系是没有维护的!

         此时,就会出现线程安全问题。那么我们如何解决呢? 当然加锁是简单粗暴的。我们只允许一个生产者进入到这个队列中,或者一个消费者进到这个队列中,两者互不干涉地并发运行。

 

锁: 

        诶,你这样不对啊。基于阻塞队列的Push也是先加锁,再获取条件变量,而你这样一来又先加锁,再访问信号量。这不都是一个操作吗?都避免不了加锁的厄运~是的,这样写是有问题的。

信号量的P、V操作需要你加锁嘛?? 它自己就保证原子性,你需要保证它的线程安全吗???

        你无非是要保证多个生产者不能访问同一个位置!

    void Push(const T &in)
    {
        // 生产者进来申请信号量
        P(_SpaceNum);

        // PV操作后加锁
        pthread_mutex_lock(&_pmutex);
        _queue[_ProducerStep++] = in;
        _ProducerStep %= _cap;
        pthread_mutex_unlock(&_pmutex);

        V(_DataNum);
    }

    void Pop(T &out)
    {

        P(_DataNum);

        pthread_mutex_lock(&_pmutex);
        out = _queue[_ConsumerStep++];
        _ConsumerStep %= _cap;
        pthread_mutex_unlock(&_pmutex);
        
        V(_SpaceNum);
    }

        这样,基于环形队列的生产者消费者模型,在多线程的环境下是可以安全运行的。


总结:

        不管是基于阻塞队列的生产者消费者模型,还是基于环形队列的生产者消费者模型,它们的高效率都不在于Push任务、Pop任务,而是能够并发地 生产任务,消费任务。

        POSIX信号量版本的生产者消费者模型能够事先让线程知道临界资源数量使用的情况,而不需要加锁、检测,如果不满足又得在条件变量下等待、释放锁……


本篇到此结束,感谢你的阅读。

祝你好运,向阳而生~

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

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

相关文章

4款【新概念APP】对比+免费下载

4款【新概念APP】对比免费下载4款【新概念APP】对比免费下载新概念英语咖&#xff08;体积小、无广告、全免费、不能倍速播放&#xff09;新概念英语全册&#xff08;免费&#xff0c;但强制广告&#xff0c;否则不能播放音频。可以倍速&#xff09;新概念英语全四册&#xff0…

【开发工程师的运维小知识】docker安装gitlab

文章目录1 搜索gitlab的镜像2 拉取gitlab镜像3 创建挂载目录4 创建gitlab容器并启动5 查看是否启动成功6 修改配置文件7 重启gitlab8 获取root初始化密码9 修改root初始密码&#xff08;可选&#xff09;进入docker-gitlab容器内部打开控制台查找第一个User&#xff08;这个就是…

【SQL Server】数据库开发指南(一)数据库设计

文章目录一、数据库设计的必要性二、什么是数据库设计三、数据库设计的重要性五、数据模型5.1 实体-关系&#xff08;E-R&#xff09;数据模型5.2 实体&#xff08;Entity&#xff09;5.3 属性&#xff08;Attribute&#xff09;5.5 关系&#xff08;Relationship&#xff09;六…

生成式人工智能所面临的问题有哪些?

在生成式人工智能中工作需要混合技术、创造性和协作技能。通过发展这些技能&#xff0c;您将能够在这个令人兴奋且快速发展的领域应对具有挑战性的问题。 生成式人工智能是指一类机器学习技术&#xff0c;旨在生成与训练数据相似但不完全相同的新数据。 换句话说&#xff0c;…

苹果6信号不好的快速解决方法

许多朋友反馈&#xff0c;苹果6的信号不佳&#xff0c;建议从以下方面查找&#xff1a; 方法一&#xff1a;开启飞行模式后再关闭 有时候手机由于周围环境网络比较差&#xff0c;会导致信号处于无服务状态&#xff0c;这时后我们开启飞行模式后再关闭飞行模式&#xff0c;系统就…

【多线程与高并发(锁)】1、锁的概念、分类和状态

1、锁的概念 java当中的锁、是在多线程环境下为保证共享资源健康、线程安全的一种手段。 线程操作某个共享资源之前&#xff0c;先对资源加一层锁&#xff0c;保证操作期间没有其他线程访问资源&#xff0c;当操作完成后&#xff0c;再释放锁。 2、锁的分类 Java中的锁按照…

Obsidian:实现日记记录【设计并使用模板】

问题背景 我是一个比较喜欢记录的人&#xff0c;有一定的写日记的习惯的&#xff0c;但是我又不太喜欢将自己的个人的数据寄人篱下&#xff0c;放在别人的数据库中。 于是就想着将自己的日记存放在自己本地的磁盘中…… 在一次偶然在B站中翻找资料时&#xff0c;我发现了这个…

Linux-Shell设计

一、shell 总论 ​ shell 就是“壳程序”&#xff0c;这个名字是针对 kernel 来说的&#xff0c;也就是在操作系统外围的程序&#xff08;严格的讲&#xff0c;已经不是操作系统了&#xff09;。宏观上的 shell 是所有的应用程序&#xff0c;而狭义上的 shell&#xff0c;指的…

STM32CubeMXA安装和创建项目

STM32CubeMXA安装和创建项目 安装STM32CubeMXA STM32CubeMX 运行环境搭建包含两个部分。首先是 Java 运行环境安装&#xff0c;其次是 STM32CubeMX 软件安装。 安装 JAVA 环境 对于 Java 运行环境&#xff0c;大家可以到 Java 官网 www.java.com 下载最新的 Java 软件 安装…

CSS 扫盲

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录引入方式内部样式内联样式外部样式CSS 选择器CSS 常用属性值字体属性设置字体大小粗细文字样式文本属性文本颜色文本对齐文本装…

使用Jmeter进行http接口测试

前言&#xff1a; 本文主要针对http接口进行测试&#xff0c;使用Jmeter工具实现。 Jmter工具设计之初是用于做性能测试的&#xff0c;它在实现对各种接口的调用方面已经做的比较成熟&#xff0c;因此&#xff0c;本次直接使用Jmeter工具来完成对Http接口的测试。 一、开发接口…

【Unity项目实战】从零手戳一个背包系统

首先我们下载我们的人物和背景资源,因为主要是背包系统,所以人物的移动和场景的搭建这里我们就不多讲了,我这里直接提供基础项目源码给大家去使用就行 基础项目下载地址: 链接: https://pan.baidu.com/s/1o7_RW_QQ1rrAbDzT69ApRw 提取码: 8s95 顺带说一下,这里用到了uni…

uniCloud开发api接口服务

首先创建一个云对象&#xff1a; 在创建的云对象的index.Obj.js中进行编码&#xff1a; const db uniCloud.database() module.exports {_before: function () { // 通用预处理器},async get(){//demo-user 是云数据中的一个表名let res await db.collection("demo-us…

最易学和最难学编程语言排行榜!

如果问一个程序员最容易学习的语言&#xff0c;就像问一个人他们最喜欢的冰淇淋。每个人都有自己的偏好&#xff0c;永远没有真正的正确答案。 正如开发者和教育家 Marek Zaluski 曾经说的那样&#xff0c;"编程语言是由程序员创造的&#xff0c;为程序员服务"。这几…

Hashtable是什么?它和Hashmap有什么区别?

博主简介&#xff1a;努力的打工人一枚博主主页&#xff1a;xyk:所属专栏: JavaEE初阶目录 一、什么是Hashtable&#xff1f; 二、Hashtable特点 2.1 Hashtable是怎么加锁的&#xff1f; 2.2Hashtable为什么不允许键值为null&#xff1f; 2.3Hashtable为什么线程安全&…

电动汽车热管理方案

热管理技术作为汽车节能、提高经济性和保障安全性的重要措施&#xff0c;在汽车研发过程中具有重要作用。传统燃油汽车的热管理系统主要包括发动机、变速器散热系统和汽车空调&#xff0c;而电动汽车的热管理系统在燃油汽车热管理架构的基础之上&#xff0c;又增加了电机电控热…

Docker实现MySQL8主从读写分离【超简洁】

1、首先拉取镜像 docker pull mysql 2、创建主库容器 docker run -p 3388:3306 --name master -e MYSQL_ROOT_PASSWORD123456 -d mysql --server-id1 --log-binbin-log --binlog-do-dbznzm-dlaq 说明&#xff1a; docker run 表示创建并运行容器-p 3388:3306 把宿主机的…

【SQL开发实战技巧】系列(四十):Oracle12C常用新特性☞可以在同样的列(列组合)上创建多个索引以及可以对DDL操作进行日志记录

系列文章目录 【SQL开发实战技巧】系列&#xff08;一&#xff09;:关于SQL不得不说的那些事 【SQL开发实战技巧】系列&#xff08;二&#xff09;&#xff1a;简单单表查询 【SQL开发实战技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL开发实战技巧…

软件测试岗,4 轮面试成功拿下字节 Offer..........

一共经历了四轮面试&#xff1a;技术4面&#xff0b;HR面。 特整理出所涉及的全部知识点&#xff0c;并复盘了完整面试题及答案&#xff0c;分享给大家&#xff0c;希望能够帮到一些计划面试字节的朋友。 一、测试基础理论类 怎么编写案例?软件测试的两种方法测试结束的标准…

python好玩的短代码

Python语言是一种流行的编程语言&#xff0c;在 Python语言中有很多有趣的特性&#xff0c;比如&#xff1a; 1.变量可以定义为字符串&#xff0c;也可以定义为字符串对象 2.变量可以用来初始化一个函数或模块&#xff0c;函数或者模块可以定义成一个类&#xff0c;这个类被称为…
最新文章