【Linux】多线程同步--基于环形队列的生产者消费者模型(信号量解决)

信号量

什么是信号量?

本质信号量就是一个计数器,它表示临界资源的数量,也就是说它描述的是有多少临界资源可以分配给线程去访问;

对于临界资源来说,假如我们可以把它在细分多个小的资源区域,如果我们有某总手段处理得当,也是可以让多个线程同时访问临界资源的不同区域,从而实现并发的效果;

每个线程在访问临界资源时候,首先必须先申请信号量资源,申请成功才可以进入临界资源;
一旦申请成功进入临界资源区域,那么也就表示内部一定有一个小区域是可以给你线程访问的;

总的来说:信号量本质就是一把计数器,能够达到对临界资源预定的目的;


信号量基本原理伪代码

如何保证信号量是原子的操作呢?只需要加锁就可以完美解决这个问题;

都说信号量本质是计数器,线程进入临界区资源都需要先申请信号量资源,申请信号量资源就是对齐信号进行-1操作,而这个-1的操作本身不是原子的,为了保证原子就必须加锁;而我们的信号量资源可能不被申请成功,那么该线程就必须挂起等待了;

同理当线程离开临界资源区时候,要归还信号量资源,也就是对其+1操作,但是这个操作也不是原子的,所以也必须加锁!


在这里插入图片描述


POSIX信号量操作函数

  1. 信号量的初始化和销毁函数
    在这里插入图片描述

  1. 信号量的PV操作函数

在这里插入图片描述


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

  1. 生产者和消费者刚开始就是指向相同的位置;队列为空时候,也是同一位置,队列为满的时候,也是同一位置;
  2. 那么队列不为空,也不为满,生产者 和消费者一定指向的不是同一个位置;这么说,对于生产者和消费者来说,他们是可以并发执行的,也就是可以一个生产的时候,另一个消费,也不相互影响对方;
  3. 当我们的队列为空或者为满的时候,不能让生产者和消费者同时进行,也就是说,生产者和消费者此时表现关系为竞争关系;
  4. 当队列为空时候,我们应该让生产者先生产;当队列为满,我们应该让消费者先消费;此时生产者和消费者体现的关系为协作关系;

那么如何实现呢?实现的基本思想:
对于生产者最关心的是什么?关心的是环形队列的空位置有多少。
对于消费者最关心的是什么?关心的是环形队列的数据(产品)有多少。


所以我们来实现一下代码:
首先有两个文件一个是头文件:ring_queue.hpp一个是主程序的文件:mian.cc;

头文件:我们使用的是C++的vector来模拟环形队列;
初始情况:
我们这里只维护了生产者和消费者之间的竞争关系和同步关系;

因为现在我们模拟的是一个生产者和一个消费者的情况;

环形队列为空,也就是没有数据,也就是有很多空格子,而空格子就是生产者关心的资源数量;
我们初始化空格子的数量为10;也就是信号量设置10,表示生产者的资源数量;
而数据的信号量设置为0,因为一开始是没有数据的;


首先是ring_queue.hpp文件:

#pragma once
#include <vector>
#include <semaphore.h>
const int _g_cap_default = 10;

template <typename T>

class RingQueue
{
    
    
private:
    std::vector<T> _ring_queue;
    int _cap;
    /*
    //生产者和消费者之间关系:竞争和同步
    如何维护呢?
    搞两个信号量
    1.生产者只关心临界区里面空格子的数量,因为生产者需要放数据
    2.消费者值关心理解去里面数据的数量 ,因为消费者需要消费数据
    */

    sem_t _blanks_sem; //生产者关心的临界资源是空格子的数目
    sem_t _data_sem;   // 消费者关心的临界资源是数据

    int _producer_index; //生产者的环形队列下标
    int _consumer_index; //消费者的环形队列下标
public:
    RingQueue(int cap = _g_cap_default) : _ring_queue(cap), _cap(cap)
    {
    
    
        sem_init(&_blanks_sem, 0, cap); // 生产者刚开始有的格子的个数
        sem_init(&_data_sem, 0, 0);     // 消费者刚开始是没有数据消费的

        _producer_index = 0;
        _consumer_index = 0;
    }
    ~RingQueue()
    {
    
    
        sem_destroy(&_blanks_sem);
        sem_destroy(&_data_sem);
    }

public:
    void push(const T &in)
    {
    
    
        //生产者生产数据第一件事是:申请临界区的资源
        sem_wait(&_blanks_sem); // p(空格子)

        _ring_queue[_producer_index] = in;

        //生产者生产完数据后,就可以增加消费者的数据资源啦!
        sem_post(&_data_sem); // V(数据)

        _producer_index++;
        _producer_index %= _cap;
    }
    void pop(T *out)
    {
    
    
        //消费数据的第一件事:申请临界区的资源
        sem_wait(&_data_sem);

        *out = _ring_queue[_consumer_index];

        //消费者消费完数据,就可以增加生产者的空格子资源啦!
        sem_post(&_blanks_sem);

        _consumer_index++;
        _consumer_index %= _cap;
    }
};

主程序:main.cc

#include <iostream>
#include <pthread.h>
#include "ring_queue.hpp"
#include <time.h>
#include<unistd.h>
using namespace std;

void *consumer(void *args)
{
    
    
    RingQueue<int> *ring_queue = (RingQueue<int> *)args;
    while (true)
    {
    
    
        sleep(1);
        int data = 0;

        ring_queue->pop(&data);
        cout << "消费者线程: " << pthread_self() << "消费的的数据是:" << data << endl;
    }
}
void *producer(void *args)
{
    
    
    RingQueue<int> *ring_queue = (RingQueue<int> *)args;
    while (true)
    {
    
    
        //生产者生产产品的数据来源:这个场景就是随机数
        int data = rand() % 20 + 1;
        cout << "生产者线程:" << pthread_self() << "生产的数据是:" << data << endl;
        ring_queue->push(data);
    }
}
int main()
{
    
    
    srand((long long)time(NULL));

    RingQueue<int> *ring_queue = new RingQueue<int>();

    pthread_t comsumer_tid;
    pthread_t producer_tid;

    pthread_create(&producer_tid, NULL, producer, (void *)ring_queue);
    pthread_create(&comsumer_tid, NULL, consumer, (void *)ring_queue);

    pthread_join(producer_tid, nullptr);
    pthread_join(comsumer_tid, nullptr);
    return 0;
}

执行的结果:
由于我设计了消费者线程先睡一秒,所以生产者就先获得CPU调度,马上生产完了一批数据,然后消费者来消费,然后就一步一步的运行了;
在这里插入图片描述


上面的只是完成了一个生产者和一个消费者的生产者消费者模型;
但是实际上:我们是有多个生产者和多个消费者的;
所以我们还需要维护生产者和生产者的竞争关系,消费者和消费者的竞争关系;
如何维护呢?
只要加锁就可以解决;

所以我们修改了上面的ring.queue.hpp的代码

#pragma once
#include <vector>
#include <semaphore.h>
#include<pthread.h>
const int _g_cap_default = 10;

template <typename T>

class RingQueue
{
    
    
private:
    std::vector<T> _ring_queue;
    int _cap;
    /*
    //生产者和消费者之间关系:竞争和同步
    如何维护呢?
    搞两个信号量
    1.生产者只关心临界区里面空格子的数量,因为生产者需要放数据
    2.消费者值关心理解去里面数据的数量 ,因为消费者需要消费数据
    */

    sem_t _blanks_sem; //生产者关心的临界资源是空格子的数目
    sem_t _data_sem;   // 消费者关心的临界资源是数据

    /*
        在多生产多消费线程的模型下
        _producer_index和_consumer_index也会成为临界资源
        _producer_index 是生产者和生产者之间的临界资源
        _consumer_index 是消费者和消费者之间的临界资源
    */
    int _producer_index; //生产者的环形队列下标
    int _consumer_index; //消费者的环形队列下标

    pthread_mutex_t _consumer_mutex; //维护消费者和消费者之间线程竞争资源的锁
    pthread_mutex_t _producer_mutex; //维护生产者和生产者之间线程竞争资源的锁
public:
    RingQueue(int cap = _g_cap_default) : _ring_queue(cap), _cap(cap)
    {
    
    
        sem_init(&_blanks_sem, 0, cap); // 生产者刚开始有的格子的个数
        sem_init(&_data_sem, 0, 0);     // 消费者刚开始是没有数据消费的

        _producer_index = 0;
        _consumer_index = 0;

        pthread_mutex_init(&_consumer_mutex, NULL);
        pthread_mutex_init(&_producer_mutex, NULL);
    }
    ~RingQueue()
    {
    
    
        sem_destroy(&_blanks_sem);
        sem_destroy(&_data_sem);

        pthread_mutex_destroy(&_consumer_mutex);
        pthread_mutex_destroy(&_producer_mutex);
    }

public:
    void push(const T &in)
    {
    
    
        /*
            信号量解决的是:临界资源的数目还剩多少
            锁解决的是:保证临界资源的生产者只有一个在访问
        */
        //生产者生产数据第一件事是:申请临界区的资源
        sem_wait(&_blanks_sem); // p(空格子)

        pthread_mutex_lock(&_producer_mutex); //这个锁主要是解决生产者和生产者之间的竞争关系的

        _ring_queue[_producer_index] = in;
        _producer_index++;
        _producer_index %= _cap;

        pthread_mutex_unlock(&_producer_mutex);
        //生产者生产完数据后,就可以增加消费者的数据资源啦!
        sem_post(&_data_sem); // V(数据)
    }
    void pop(T *out)
    {
    
    
          /*
            信号量解决的是:临界资源的数目还剩多少
            锁解决的是:保证临界资源的消费者只有一个在访问
        */
        //消费数据的第一件事:申请临界区的资源
        sem_wait(&_data_sem);
        pthread_mutex_lock(&_consumer_mutex); //这个锁主要是解决消费者和消费者之间的竞争关系的

        *out = _ring_queue[_consumer_index];
        _consumer_index++;
        _consumer_index %= _cap;

        pthread_mutex_unlock(&_consumer_mutex);

         //消费者消费完数据,就可以增加生产者的空格子资源啦!
        sem_post(&_blanks_sem);
    }
};

最主要的修改部分:就是加多了一把锁维护了生产者和生产者之间竞争关系;消费者和消费者之间的竞争关系;
因为当有多生产者的时候,每个生产者就会同时访问临界资源,需要对齐进行互斥访问;
消费者同理;

为什么这里的P操作时在上锁的操作之前?

因为我们知道P操作就是对访问临界资源起了一个预定的机制;当有多生产者进来时候,我们多个生产者都有机会预定访问临界资源,但是只有一个生产者有资格进入临界资源;
在这里插入图片描述


在这里插入图片描述


而我们的mian.cc的代码主要从原来的单生产单消费变成了多生产多消费;
这里模拟了3个生产三个消费的线程;
在这里插入图片描述


执行的结果如下:
发现确实时有3个生产者和3个消费者并发的进行了;

在这里插入图片描述


总结

还是那句话:生产者和消费者模型;关心的不仅仅是如何把数据放入队列,和如何从队列拿出数据;
我们更加要关心:数据到来生产者的时间和消费者拿出数据处理的时间问题;

上面的代码示例我都没演示:数据到来生产者的时间和消费者拿出数据处理的时间问题;

只是演示如何把数据放入队列,和如何从队列拿出数据;

其实只要把上面的的main.cpp的代码稍微改一下就可以了,我们知道不往队列放入整数,比如放入一个任务,那么这个任务就会有数据来源和数据处理的过程了!!!!


猜你喜欢

转载自blog.csdn.net/m0_46606290/article/details/124899799