Linux:多线程---深入生产消费模型&&环形队列生产消费模型

文章目录

      • [1. 生产者消费者模型](#1. 生产者消费者模型)
        • [1.1 深入生产消费模型](#1.1 深入生产消费模型)
        • [1.2 条件变量误唤醒](#1.2 条件变量误唤醒)
      • [2. POSIX信号量](#2. POSIX信号量)
        • [2.1 信号量的概念](#2.1 信号量的概念)
        • [2.2 信号量的接口](#2.2 信号量的接口)
      • [3. 环形队列生产消费模型](#3. 环形队列生产消费模型)
        • [3.1 环形队列的概念](#3.1 环形队列的概念)
  • 序:在上一章中,我们通过同步的概念了解了条件变量的概念,并且对生产者消费者模型有了一定的认识,但仅仅是这样,我们对于生产者消费者模型的认识还是太浅显了,所以本章将深入生产者消费者模型,并用POSIX信号量来实现环形队列生产者消费者模型

1. 生产者消费者模型

1.1 深入生产消费模型

问题一:生产者的数据从哪里来?

用户,网络等,生产者生产的数据也是要花时间的!
消费者要不要对拿到的数据进行数据加工处理?要的,这也是要花时间的。

生产者:
1. 获取数据
2.生产数据到队列
消费者:
1. 消费数据
2. 对数据进行加工处理

问题二:生产消费模型为什么高效?生产消费模型,不是生产几个,然后消费几个吗?哪怕有不同的生产和消费策略,但是这哪里高效了?

当生产者持有锁时,他在生产数据,此时消费者难道就只能等待生产者生产数据到一个临界点,甚至生产满了吗?答案显然不是,消费者可以在未持有锁的阶段去将之前消费的数据进行加工处理呀!!!同理,当消费者持有锁时,生产者也不会傻傻的等待消费者将数据消费完,生产者会去获取数据!!!等到生产者持有锁时,再接着生产数据到队列内!!!所以这才是生产消费模型高效的原因,无论是生产者还是消费者,他们每时每刻都在完成任务,将空闲的时间利用上,这就是高效!!!

1.2 条件变量误唤醒

问题一:如果线程wait时,被误唤醒了呢??


假设此时最多能够容纳10个资源,现在已经有9个了,如果调用pthread_cond_broadcast函数唤醒全部线程,三个线程就会对锁展开竞争,其中一个线程就会持有锁,然后生产一个资源后再释放锁,而另外两个线程此时已经不在等待队列了,而是在锁的队列下进行等待,此时锁被释放,两个线程中的其中一个就会得到锁,继续执行生产资源的任务,然后此时的资源已经满了(这种情况就叫做伪唤醒或者误唤醒),此时的另外两个线程,拿到锁后,并不是从位置1开始进行的,而是直接从位置2开始执行代码,这就会导致没有进行判断这一步,因为多个线程被唤醒后,这些线程就不在条件变量下等待了,所以访问的有序性也就没了。
而要解决这个问题,也很简单,只需要将if的判断条件换成while,这样即使是有伪线程,也会让伪线程再次进行判断,然后再次进入等待队列中进行等待。

2. POSIX信号量

2.1 信号量的概念

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

2.2 信号量的接口

初始化信号量:

对信号量进行初始化

第一个参数:传入系统定义的数据结构sem_t
第二个参数:是否为线程间共享,直接默认传0
第三个参数:表示信号量的数量(本质上是计数器)

销毁信号量:

释放信号量资源

第一个参数:直接传系统定义的数据结构sem_t

等待信号量:

获取信号量资源,对应信号量进行减减操作 --- P操作

这里只讨论sem_wait函数,第一个参数直接传系统定义的数据结构sem_t

发布信号量:

释放信号量资源,对信号量进行加加操作 --- V操作

第一个参数直接传系统定义的数据结构sem_t
信号量的使用:p()操作 ---> //访问资源 ---> V()操作 ---> //释放资源

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

3.1 环形队列的概念

用数组模拟环形队列,有tail和head,head位置是添加元素的位置,满了和空的时候,tail和head指向的是同一个位置,所以无法判断是空还是满,第一种方法是添加计数器,第二种方法是空一个位置,加个判断temp=head+1;temp%20;判断head的下一个位置是不是tail,如果是就生产满了,就不允许生产了,反之则可以生产

其中消费者和生产者都是顺时针进行生产和消费,并且满足以下规则:

1. 指向同一个位置,只能由一个人访问(空:只能由生产者来访问环形队列,满:只能由消费者来访问环形队列)
2. 消费者不能超过生产者
3. 生产者不能把消费者套一个圈
只有当空和满的时候,生产者和消费者才会在同一个位置,当不空和不满的时候,生产者和消费者就不在同一个位置。
生产者关注什么资源?关注环形队列还有多少剩余空间(最开始SpaceSem:N)
消费者关注什么资源?关注环形队列还有多少剩余数据(最开始DataSem:0)
环形队列代码如下:

复制代码
#pragma once

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

template<class T>
class RingQueue
{
const static int defuatcap = 50;
private:
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }

    void V(sem_t &sem)
    {
        sem_post(&sem);
    }

    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }

    void Unlock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);  
    }

public:
    RingQueue(int cap = defuatcap)
    :_ringqueue(cap)
    ,_cap(cap)
    ,_c_step(0)
    ,_p_step(0)
    {
        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);
        sem_init(&_c_data_sem,0,0);
        sem_init(&_p_space_sem,0,_cap);
    }

     void Push(const T &in)//生产
    {
        P(_p_space_sem);
        
        Lock(_p_mutex);
        _ringqueue[_p_step] =in;
        //位置后移,维持环形特征
        _p_step++;
        _p_step%=_cap;
        Unlock(_p_mutex);
        
        V(_c_data_sem);

    }

    void Pop(T *out)//消费
    {
        P(_c_data_sem);

        Lock(_c_mutex);
        *out = _ringqueue[_c_step];
        //位置后移,维持环形特征
        _c_step++;
        _c_step%=_cap;
        Unlock(_c_mutex);

        V(_p_space_sem);
    }

    ~RingQueue()
    {
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
        sem_destroy(&_c_data_sem);
        sem_destroy(&_p_space_sem);
    }

private:
    std::vector<T> _ringqueue;
    int _cap;
    int _c_step;//消费者下标
    int _p_step;//生产者下标
    sem_t _c_data_sem;//消费者关注的数据资源
    sem_t _p_space_sem;//生产者关注的空间资源
    pthread_mutex_t _p_mutex;
    pthread_mutex_t _c_mutex;
};

当环形队列为空时,只能进行生产动作,不能进行消费动作
当环形队列为满时,只能进行消费动作,不能进行生产动作
当环形队列不空不满时,生产者和消费者就实现了并发访问,能够同时进行
其中,对于先加锁还是先申请信号量,有两种方案:
方案一

方案二

首先,信号量的操作是原子的,是不需要加锁的,临界区代码能少就少,其次,如果先申请锁再申请信号量,如果信号量没有申请到,就会白费时间,而如果先进行申请信号量再申请锁,假设,有一个线程已经申请到锁了,正在进行生产,此时其他线程进来了,就不会干等待,等待锁的就绪,而是会先申请信号量,如果申请到了,只要锁一释放就能立马进行生产操作!!!,只有抢到了信号量资源,才需要被加锁,如果连信号量资源都没有申请到,就没必要加锁,加锁也是很耗资源的,所以选择第二个方案。
通俗点讲,如果将信号量比作票数,锁比作一个门,如果先排队,到门口再进行买票,就会出现排了半天发现没票了,进不去门里面,浪费了时间,而如果先买了票,让买了票的人进行排队,只要排到了就能立马进入门里面。
当环形队列不为空和不为满的时候,生产者和消费者只需要维持自身的互斥关系,不需要考虑对方。

总结:

本章深入探讨了生产者消费者模型,引入了生产者获取生产资料和消费者对消费数据进行加工的概念,而后又通过信号量实现了环形队列下的生产消费模型,深入探讨了加锁和PV操作的优先级的问题。

相关推荐
网易独家音乐人Mike Zhou35 分钟前
【Linux应用】在PC的Linux环境下通过chroot运行ARM虚拟机镜像img文件(需要依赖qemu-aarch64、不需要重新安装iso)
linux·c语言·stm32·mcu·物联网·嵌入式·iot
Ronin3051 小时前
【Linux系统】进程控制
linux·运维·服务器·ubuntu
渡我白衣1 小时前
Linux操作系统之线程(三)
linux
-曾牛2 小时前
Linux搭建LAMP环境(CentOS 7 与 Ubuntu 双系统教程)
linux·运维·ubuntu·网络安全·渗透测试·centos·云计算运维
小嵌同学2 小时前
Linux 内存管理(2):了解内存回收机制
linux·运维·arm开发·驱动开发·嵌入式
伊织code2 小时前
Streamlit 官翻 2 - 开发指南 Develop Concepts
缓存·框架·应用·streamlit·状态·表单·执行
绵绵细雨中的乡音2 小时前
消息队列与信号量:System V 进程间通信的基础
linux
简CC2 小时前
Linux——文件压缩和解压
linux·运维·服务器
cq_run3 小时前
centos9部署jdk
linux·运维·服务器
fly spider3 小时前
12.缓存四件套
缓存