【Linux系统编程二十九】基于信号量的环形队列生产消费模型

这篇具有很好参考价值的文章主要介绍了【Linux系统编程二十九】基于信号量的环形队列生产消费模型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一.信号量

当共享资源被当成整体使用时,则共享资源的数量要么是1,要么是0。
当被访问使用时,共享资源的数量就为0,当没有被使用时,数量就为1。

共享资源是可以被分成多份来使用的,只不过不同的线程访问的是该共享资源的不同的区域,它是允许多个线程并发访问的,只不过访问的是不同的区域。而信号量就是用来表示这个大的共享资源被分成多少份小的资源的个数。
信号量本质就是一把计数器,用来描述资源数量的多少。
信号量表示对资源的预定机制,只要你信号量申请成功了,就表明你肯定可以使用这块资源。不需要再判断资源是否就绪,因为在申请信号量的时候就已经判断好了,申请成功就可以使用,申请失败就不能使用就去挂起等待。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

互斥锁的本质也就是信号量,只不过是二元信号量,结果要么是1要么是0。申请到锁了,就可以去访问共享资源,访问是能访问,单是使用是有使用条件的,如果使用条件不满足,也无法使用。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

sem_t 就是信号量类型,使用它需要初始化:
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

1.P操作

P操作就是申请信号量,也就是先预定关心的资源。因为信号量本质就是一把计数器,用来描述临界资源数量的多少,而申请一个信号量,就是预定临界资源,对应的临界资源总数就要减一。
申请成功就可以使用该资源,申请失败,就说明没有资源给你使用,那么就等待。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

2.V操作

V操作就是释放信号量,也就是释放对应的资源,对应的临界资源总数就要加一。【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

二.环形队列

利用环形队列实现的生产消费模型需要满足几个条件:
首先生产者往队列里投入数据时,生产者就往后走。
消费者从队列里拿走数据时,消费者就往后走。
当队列为空,为满的时候,生产和消费是在同一个位置上的,而不为空,不为满的时候,生产和消费一定指向不同的位置。

1.当队列为空或者为满时,生产者和消费者都在同一个位置,而为了满足321原则,生产者和消费者之间必须互斥,所以只允许一个人访问队列。
并且为空的时候,肯定是生产者先访问,而消费后访问。当为满时,肯定是消费者先访问,生产者后访问。
2.而当队列不为空,不为满时,生产和消费一定指向的是不同的位置,这时生产和消费可以同时访问队列。

原则1:当实指向同一个位置时,只能有一个人访问
原则2:消费者是不能超过生产者的,生产者生产多少数据,消费者最多只能消费多少数据
原则3:生产者不能将消费者套一个圈,不然原先的数据就会被覆盖。

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++
在模型中我们利用vector数组来模拟环形队列。

三.单生产单消费场景

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

生产者关注的资源是什么呢?消费者关心的资源是什么呢?

生产者关注的是队列里是否还有空间,关注的是空间资源。
消费者关注的是队列里是否还有数据,关注的是数据资源。
而一开始,队列里并没有数据资源,但空间资源是有的,就是队列的大小。

因为信号量是描述临界资源的数量,所以我们就可以定义两个信号量分别表示对应的,空间资源,数据资源的多少。

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

【1】生产者要想往队列里生产,需要先申请空间资源,也就是空间信号量。申请成功后,就可以进行生产,生产完毕后,当前的空间被生产者生产的数据着,并且队列数据多了一个,所以需要释放数据资源,也就是释放数据信号量。如果申请信号量失败,那么就等待挂起。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

【2】消费者想要从队列里消费,需要先申请数据资源,也就是数据信号量。申请成功后,就可以进行消费,消费完毕后,数据被消费者拿走,而当前的空间多了一个,所以需要释放空间资源,也就是释放空间信号量。如果申请失败,那么就等待挂起。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

1.信号量维持生产消费之间互斥同步

在环形队列生产模型中,我们要探讨的三种关系,其中生产者和消费者之间的互斥和同步关系是由信号量来维持的。

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

#include <pthread.h>
#include <iostream>
#include <string>
#include <unistd.h>
#include <ctime>
#include "RingQueue.hpp"
#include "TASK.hpp"
struct ThreadData
{
    RingQueue<TASK>* RQ;
    std::string name;
};
void *Consumer(void *args)
{
   
    ThreadData *td=static_cast<ThreadData*>(args);
    RingQueue<TASK> *rq=td->RQ;

    while (true)
    {
        //sleep(2);
        // 1.消费数据
        TASK t;
        rq->Pop(&t);

        // 2.处理数据
        t.run();
        std::cout << "Comsumer get task,task is: " << t.GetTASK() <<"who: "<<td->name<< " reslut :" << t.Getresult() << std::endl;
    }
    return nullptr;
}

void *Producer(void *args)
{
    ThreadData *td=static_cast<ThreadData*>(args);
    RingQueue<TASK> *rq=td->RQ;

    int len = opera.size();
    while (true)
    {
       
        // 1.获取数据
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 10;
        char op = opera[rand() % len];
        // 2.生产数据
        TASK t(data1, data2, op);
        rq->Push(t);
        std::cout << "Productor task done,task is: " << t.GetTASK() <<" who: "<<td->name<< std::endl;
         sleep(2);
    }
    return nullptr;
}


int main()
{

       srand(time(nullptr));
        RingQueue<TASK> *rq = new RingQueue<TASK>();
        pthread_t c, p;
        ThreadData* td=new ThreadData();
        td->name="Procductor-"+std::to_string(i);
        td->RQ=rq;
        pthread_create(p + i, nullptr, Producer, td);
  //在创建线程的时候可以给线程定义基本的属性比如名字,以参数的形式传给线程
        pthread_create(c + i, nullptr, Consumer, td);
   
        pthread_join(c[i], nullptr);
        pthread_join(p[i], nullptr);
    
}

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
// 先模拟单生产单消费
const static int defaultcap = 5; // 默认大小
template <class T>
class RingQueue
{

public:
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }
    void V(sem_t &sem)
    {
        sem_post(&sem);
    }
    RingQueue(int cap = defaultcap) : _ringqueue(cap), _cap(cap), c_step(0), p_step(0)
    {
        sem_init(&cdata_sem, 0, 0);
        sem_init(&pspace_sem, 0, cap);
    }
    ~RingQueue()
    {
        sem_destroy(&cdata_sem);
        sem_destroy(&pspace_sem);

        pthread_mutex_destroy(&c_mutex);
        pthread_mutex_destroy(&p_mutex);
    }
//注意的是,对下标加锁,应该在申请信号量后面,从两个方面解释:信号量不需要保护,本身就是原子的。
//先申请信号量,后加锁可以提高效率,一个并发度,别人正在执行时,其他线程还可以申请信号量
    void Push(const T &in)
    {
        // 生产者在生产数据之前,需要先申请对应的信号量,申请成功说明可以生产数据
        P(pspace_sem); // 生产者申请空间资源
     
        _ringqueue[p_step] = in;
        p_step++;
        p_step %= _cap; // 维持环形
       
        V(cdata_sem); // 格子已经被占了,数据多了一个,所以释放数据资源
    }

    void Pop(T *out)
    {
        // 消费者在消费数据之前,也需要申请对应的信号量,申请成功,说明可以消费

        P(cdata_sem); // 消费者申请数据资源
        *out = _ringqueue[c_step];
        c_step++;
        c_step %= _cap; // 维持环形
        V(pspace_sem); // 拿走一个数据,空间资源多出来一个
    }

private:                       // 环形队列的基本属性
    std::vector<T> _ringqueue; // 用vector来模拟环形队列
    int _cap;                  // 该环形队列的最大值

    int c_step; // 消费者下标
    int p_step; // 生产者下标

    // 消费者和生产者各自关心的资源

    sem_t cdata_sem;  // 消费者关心的数据资源
    sem_t pspace_sem; // 生产者关心的空间资源
};
#pragma once
#include <iostream>
#include <string>

std::string opera="+-*/%";
class TASK
{

public:
    TASK()
    {}
    TASK(int data1, int data2, char op) : _data1(data1), _data2(data2), _oper(op)
    {
    }
    void run()
    {
        switch (_oper)
        {
        case '+':
            _result = _data1 + _data2;
            break;
        case '-':
            _result = _data1 - _data2;
            break;
        case '*':
            _result = _data1 * _data2;
            break;
        case '/':
        {
            if (_data2 == 0)
                _exitcode = 1;
            else
                _result = _data1 / _data2;
        }
        break;
        case '%':
        {
            if (_data2 == 0)
                _exitcode = 2;
            else
                _result = _data1 % _data2;
        }
        break;

        default:
        _exitcode=3;
            break;
        }
    }

    std::string GetTASK()
    {
       std::string r=std::to_string(_data1);
       r+=_oper;
       r+=std::to_string(_data2);
       r+="=?";
       return r;
    }
    std::string Getresult()
    {
        std::string result=std::to_string(_data1);
        result+=_oper;
        result+=std::to_string(_data2);
        result+='=';
        result+=std::to_string(_result);
        result+="[code:";
        result+=std::to_string(_exitcode);
        result+=']';
        return result;
    }

private:
    int _data1;
    int _data2;
    char _oper;

    int _result;
    int _exitcode;
};

四.多生产多消费场景

在单生产单消费模型中,生产者只有一个,而当多生产时,就可能出现问题。
比如生产过程中Push,同时进入多个生产者,集中申请信号量,因为下标只有一个,那么线程就会对同一个下标进行竞争,A生产者要往队列该下标里写,B生产者也要往队列该下标里写,最后就会造成数据的覆盖丢失。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

在环形队列里,不能存在多个生产者和多个消费者同时在环形队列的情况。这就要求生产者和生产者之间要进行互斥,消费者和消费者之间要进行互斥。
所以需要锁来实现它们之间的互斥,因为在大部分情况下,生产者和消费者不是在同一个位置上,所以可以并发执行,这时如果只设置一把锁,就会降低并发度,因为这两种关系的线程是关系不同的资源,所以使用两把锁,来加锁不同的资源。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

1.加锁维持生产生产,消费消费互斥

所以加锁是为了保护环形队列下标不被多线程并发访问。
不过这里就存在一个问题:加锁是在申请信号量之前加锁呢?还是在申请信号量之后加锁呢?

答案:在申请信号量之后加锁。为什么?
原因1:因为申请信号量本身就是原子的,不需要保护。
原因2:因为在申请信号之后加锁,可以提高效率,因为当线程持有锁执行期间,其他线程还可以进行继续申请信号量(申请成功后,就在等待锁资源,申请失败,挂起等待),等持有锁线程释放锁后,就可以直接去竞争锁。也就是多线程可以集中在申请信号量这里,先将资源预定好了,等有锁资源时,就可以直接访问临界资源。
所以在生产者生产的过程中,其他生产者还可以去申请信号量,提高并发度。
【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++

#include <pthread.h>
#include <iostream>
#include <string>
#include <unistd.h>
#include <ctime>
#include "RingQueue.hpp"
#include "TASK.hpp"
struct ThreadData
{
    RingQueue<TASK>* RQ;
    std::string name;
};
void *Consumer(void *args)
{
   
    ThreadData *td=static_cast<ThreadData*>(args);
    RingQueue<TASK> *rq=td->RQ;

    while (true)
    {
        //sleep(2);
        // 1.消费数据
        TASK t;
        rq->Pop(&t);

        // 2.处理数据
        t.run();
        std::cout << "Comsumer get task,task is: " << t.GetTASK() <<"who: "<<td->name<< " reslut :" << t.Getresult() << std::endl;
    }
    return nullptr;
}

void *Producer(void *args)
{
    ThreadData *td=static_cast<ThreadData*>(args);
    RingQueue<TASK> *rq=td->RQ;

    int len = opera.size();
    while (true)
    {
       
        // 1.获取数据
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 10;
        char op = opera[rand() % len];
        // 2.生产数据
        TASK t(data1, data2, op);
        rq->Push(t);
        std::cout << "Productor task done,task is: " << t.GetTASK() <<" who: "<<td->name<< std::endl;
         sleep(2);
    }
    return nullptr;
}


int main()
{

    srand(time(nullptr));
    RingQueue<TASK> *rq = new RingQueue<TASK>();
    pthread_t c[5], p[3];
    for (int i = 0; i < 3; i++)
    {
        ThreadData* td=new ThreadData();
        td->name="Procductor-"+std::to_string(i);
        td->RQ=rq;
        pthread_create(p + i, nullptr, Producer, td);
    }
    for (int i = 0; i < 5; i++)//在创建线程的时候可以给线程定义基本的属性比如名字,以参数的形式传给线程
    {
         
        ThreadData* td=new ThreadData();
        td->name="Consumer-"+std::to_string(i);
        td->RQ=rq;
        pthread_create(c + i, nullptr, Consumer, td);
    }
    

    for (int i = 0; i < 1; i++)
    {
        pthread_join(c[i], nullptr);
    }
    for (int i = 0; i < 1; i++)
    {
        pthread_join(p[i], nullptr);
    }
}

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
// 先模拟单生产单消费
const static int defaultcap = 5; // 默认大小
template <class T>
class RingQueue
{

public:
    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);
    }
    RingQueue(int cap = defaultcap) : _ringqueue(cap), _cap(cap), c_step(0), p_step(0)
    {
        sem_init(&cdata_sem, 0, 0);
        sem_init(&pspace_sem, 0, cap);

        pthread_mutex_init(&c_mutex, nullptr);
        pthread_mutex_init(&p_mutex, nullptr);
    }
    ~RingQueue()
    {
        sem_destroy(&cdata_sem);
        sem_destroy(&pspace_sem);

        pthread_mutex_destroy(&c_mutex);
        pthread_mutex_destroy(&p_mutex);
    }
//注意的是,对下标加锁,应该在申请信号量后面,从两个方面解释:信号量不需要保护,本身就是原子的。
//先申请信号量,后加锁可以提高效率,一个并发度,别人正在执行时,其他线程还可以申请信号量
    void Push(const T &in)
    {
        // 生产者在生产数据之前,需要先申请对应的信号量,申请成功说明可以生产数据
        P(pspace_sem); // 生产者申请空间资源
        
        Lock(p_mutex);
        _ringqueue[p_step] = in;
        p_step++;
        p_step %= _cap; // 维持环形
        Unlock(p_mutex);
       
        V(cdata_sem); // 格子已经被占了,数据多了一个,所以释放数据资源
    }

    void Pop(T *out)
    {
        // 消费者在消费数据之前,也需要申请对应的信号量,申请成功,说明可以消费

        P(cdata_sem); // 消费者申请数据资源
        Lock(c_mutex);
        *out = _ringqueue[c_step];
        c_step++;
        c_step %= _cap; // 维持环形
        Unlock(c_mutex);

        V(pspace_sem); // 拿走一个数据,空间资源多出来一个
    }

private:                       // 环形队列的基本属性
    std::vector<T> _ringqueue; // 用vector来模拟环形队列
    int _cap;                  // 该环形队列的最大值

    int c_step; // 消费者下标
    int p_step; // 生产者下标

    // 消费者和生产者各自关心的资源

    sem_t cdata_sem;  // 消费者关心的数据资源
    sem_t pspace_sem; // 生产者关心的空间资源

    // 如果是多个生产者,多个消费者,那么就需要用到锁
    // 因为生产者与生产者直接会竞争下标,而下标只有一个,所以需要对下标进行加锁保护,实现生产者与生产者之间的互斥。
    // 而消费者之间也会竞争下标,生产消费因为关心的是不同资源,所以各自保护各自的下标。
    pthread_mutex_t c_mutex;
    pthread_mutex_t p_mutex;
};

五.总结

【Linux系统编程二十九】基于信号量的环形队列生产消费模型,Linux系统编程,linux,服务器,c++文章来源地址https://www.toymoban.com/news/detail-813721.html

到了这里,关于【Linux系统编程二十九】基于信号量的环形队列生产消费模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】POSIX信号量 | 基于环形队列的生产者消费者模型

    ​🌠 作者:@阿亮joy. 🎆 专栏: 《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 POSIX 信号量和 SystemV 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但 POSIX 可以用于

    2023年04月08日
    浏览(57)
  • 并发编程 --- 信号量线程同步

    上文编码技巧 --- 同步锁对象的选定中,提到了在C#中,让线程同步有两种方式: 锁(lock、Monitor等) 信号量(EventWaitHandle、Semaphore、Mutex) 加锁是最常用的线程同步的方法,就不再讨论,本篇主要讨论使用信号量同步线程。 实际上,再C#中 EventWaitHandle 、 Semaphore 、 Mutex 都是

    2024年02月16日
    浏览(35)
  • 【Linux学习】多线程——信号量 | 基于环形队列的生产者消费者模型 | 自旋锁 | 读写锁

    🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言: 你只管努力,剩下的交给时间! 之前在学习进程间通信的时候,本喵简单的介绍过一下信号量,今天在这里进行详细的介绍。 这是之前写的基于阻塞队列的生产者消费者模型中向阻塞队列中push任务的代码。 上面代码

    2024年02月07日
    浏览(48)
  • 【Linux从入门到精通】信号量(信号量的原理与相关操作接口)详解

      本篇文章重点对 信号量的概念,信号量的申请、初始化、释放、销毁等操作进行讲解。同时举例把信号量应用到生产者消费者模型来理解 。希望本篇文章会对你有所帮助。 目录 一、信号量概念 1、1 什么是信号量 1、2 为什么要有信号量 1、3 信号量的PV操作 二、信号量的相

    2024年02月08日
    浏览(36)
  • linux(信号量)

    1.回顾信号量的概念 2.认识信号量对应的操作函数 3.认识一个环形队列 4.结合sem+环形队列写生产者消费者模型 --------------------------------------------------------------------------------------------------------------------------------- 1.回顾信号量的概念  每个人想进放映厅看电影,第一件事就是买票

    2024年02月11日
    浏览(34)
  • linux信号量

    通过学习linux的信号量,对linux的信号量进行了编程。

    2024年02月10日
    浏览(35)
  • 【Linux】浅谈信号量

    tips:system V 是一套标准,共享内存,信号量,消息队列属于system V。 进程A和进程B进行通信时,假如进程A向物理内存的共享区写入\\\"Hello World\\\",但是当进程A写入了\\\"Hello\\\"时,进程B就向内存读取了,所以只读取到了\\\"Hello\\\",这就导致进程A想向进程B发送的信息,进程B读取不完整,

    2024年02月05日
    浏览(38)
  • 【Linux】进程间通信 -- 信号量

    信号量是什么? 本质是一个计数器,通常用来表示公共资源中,资源数量多少的问题 公共资源:能被多个进程同时可以访问的资源 访问没有保护的公共资源:数据不一致问题(比如我想写abc123,但是我123还没有写入,就读取了abc,可能数据分开会导致数据无意义) 为什么要

    2024年02月16日
    浏览(39)
  • linux进程间通信(信号量)

    信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发 送(即 V(信号变量))信息操作。最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式, 叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号

    2024年02月07日
    浏览(29)
  • 【Linux篇】第十七篇——信号量

    前言 POSIX信号量 信号量的概念 信号量的工作原理 信号量函数 二元信号量模拟实现互斥功能 基于环形队列的生产消费模型 空间资源和数据资源 生产者和消费者申请和释放资源 必须遵守的两个规则 代码实现 信号量保护环形队列的原理 将可能被多个执行流同时访问的资源叫

    2024年02月06日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包