【Linux】信号量(基于环形队列的生产消费模型)

文章目录

  • POSIX信号量
    • 一、什么是信号量
    • 二、信号量接口
      • 1.初始化信号量
      • 2.销毁信号量
      • 3.申请信号量(等待信号量)
      • 4.释放信号量(发布信号量)
  • 基于环形队列的生产消费模型
    • 一、结构介绍
    • 二、理论讲解
    • 三、代码实现
  • 总结

POSIX信号量

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

一、什么是信号量

【Linux】信号量(基于环形队列的生产消费模型)_第1张图片
在基于阻塞队列实现生产者消费者模型时,我们发现,在进行生产和消费时,必须先加锁最后解锁,所以在进行生产和消费时,一定是互斥进行的,所有任务都是顺序进行对临界资源的访问,一个临界资源被当成整体来看待的。
但是在有的情况下,一个临界资源可以分成多个资源来使用,此时每个线程可以同时访问被分成的某一个资源,然后可以去同时处理,所以就可以实现并发,只有当多个线程来访问同一份资源时,要通过互斥同步来实现。


那我们怎么知道是否有一份资源呢?
这时就要引入信号量的概念了,信号量本质上是一把计数器,记录的是临界资源的划分的一份一份的临界资源的数目,知道这个数目,我们就可以知道是否有一份资源可以访问,只有满足条件才可以访问。

我们通过下边一个例子来更好的理解信号量:

比如在我们要看电影进行购票时,信号量就相当于电影票的预定机制,在进入放映厅之前,大家就已经买到了票,每个人拿到一张票,就意味着在放映厅内一定有一个座位属于他,但是在卖票时一定要有一定的互斥和同步机制,不能多个人购买同一张票,当票数为0时,就要停止卖票。

转换到今天我们所学的信号量来说:
电影院的所有座位就是一份临界资源,买票的每一个人都是一个线程,而每一个座位就是被划分的一小份资源,一个人买到一张票预定座位就代表着申请到信号量,在卖票时不能把同一张票卖给多个人就是要保证多个线程在访问同一份资源时要有互斥和同步机制,其实,通过信号量我们要实现的是当要看电影时是多个人进入电影院同时看电影,而不是一个人看完一个电影之后另外一个人再进去,这大大提高了效率。

二、信号量接口

1.初始化信号量

#include 
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

2.销毁信号量

int sem_destroy(sem_t *sem);
参数:
sem代表要销毁的信号量的指针

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

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

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

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

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

一、结构介绍

我们在之前学习环形链表的时候接触过这种结构,此处的环形队列本质上一个数组,在物理结构中就是一个线性的数组,而通过我们来进行取模等运算,在逻辑结构中看起来是一个环形的。
但是使用环形结构又会出现一些问题:
在之前学习环形链表时,我们发现如果不进行处理,只是单纯使用begin==end来判断,不能区分是为满还是为空,所以我们选择多开一个空间来解决。
【Linux】信号量(基于环形队列的生产消费模型)_第2张图片
而在我们这里采用计数器的方式来实现判空和判满。

二、理论讲解

我们之前提到过生产者消费者模型有一个321原则,3就是三种关系,2是2种身份,1是一个交易场所,其实环形队列实现和阻塞队列实现本质上差别就是交易场所不同。
前边提到,当我们将一份临界资源划分为多份之后,就可以让多个线程并发的访问临界资源,所以当消费者和生产者没有指向同一份资源时,他们就是并发的,当指向同一份资源时,说明此时环形队列为空或为满,必须挂起等待。

期望:

生产者不能套圈消费者
消费者不能超过生产者
当为空时,必须让生产者先运行。
当为满时,必须让消费者先运行。
除此之外的情况,都可以并发实现。

并且对于生产者来说,关心的是否有空间资源,而对于消费者来说,关心的是是否有数据资源,每当申请一个数据资源之后,相应空间资源就会加一,而每当申请一个空间资源之后,数据资源就会加一。

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

对于生产者来说,生产者每次生产数据前都需要先申请(等待)空间信号量,
如果空间信号量的值不为0,则信号量申请成功,空间信号量减一,此时生产者可以进行生产操作。
如果空间信号量的值为0,则信号量申请失败,此时生产者需要在空间信号量的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒,当生产者生产完数据后,应该释放B信号量,因为此时队列中的数据已经+1,释放(发布)数据信号量,V操作

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

对于消费者来说,消费者每次消费数据前都需要先申请(等待)数据信号量,
若数据信号量的值不为0,则信号量申请成功,数信号量减一(即等待空间信号量成功,P操作),此时消费者可以进行消费操作。
如果数据信号量的值为0,则信号量申请失败,此时消费者需要在数据信号量的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
当消费者消费完数据后,应该释放空间信号量,因为此时队列中的数据已经减一,空出一个空间资源,释放(发布)空间信号量,V操作。

三、代码实现

mainTest.cc

#include"ringQueue.hpp"
#include 
#include 
#include 
#include 
#include 
using namespace std;

void* consumer(void* args)
{
    RingQueue* rq = (RingQueue*)args;
    while(true)
    {
        sleep(1);
        int x = 0;
        rq->pop(&x);
        cout << "消费: " << x << " [" << pthread_self() << "]" << endl;
        // cout<<"消费"<* rq = (RingQueue*)args;
    while(true)
    {
        
        int in = rand()%30+1;
        cout << "生产: " << in << " [" << pthread_self() << "]" << endl;
        // cout<<"生产"<push(in);
    }
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    RingQueue* rq = new RingQueue();
    pthread_t c[3],p[2];
    pthread_create(c,nullptr,consumer,(void*)rq);
    pthread_create(c+1,nullptr,consumer,(void*)rq);
    pthread_create(c+2,nullptr,consumer,(void*)rq);

    pthread_create(p, nullptr, productor, (void*)rq);
    pthread_create(p+1, nullptr, productor, (void*)rq);

    for(int i = 0; i < 3; i++) pthread_join(c[i], nullptr);
    for(int i = 0; i < 2; i++) pthread_join(p[i], nullptr);
    return 0;
}

ringQueue.hpp

#pragma once
#include 
#include 
#include 
#include "sem.hpp"
using namespace std;

const int gDeafultSize = 5;
template 
class RingQueue
{
public:
    RingQueue(int deafultSize = gDeafultSize)
        : _queue(deafultSize), _nums(deafultSize), _spaceSem(deafultSize), _dataSem(0), c_step(0), p_step(0)
    {
        pthread_mutex_init(&cLock, nullptr);
        pthread_mutex_init(&pLock, nullptr);
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&cLock);
        pthread_mutex_destroy(&pLock);
    }

    void push(const T &x)
    {
        _spaceSem.p();
        pthread_mutex_lock(&pLock);
        _queue[p_step++] = x;
        p_step %= _nums;
        pthread_mutex_unlock(&pLock);
        _dataSem.v();
    }
    void pop(T *out)
    {
        _dataSem.p();
        pthread_mutex_lock(&cLock);
        *out = _queue[c_step++];
        c_step %= _nums;
        pthread_mutex_unlock(&cLock);
        _spaceSem.v();
    }

private:
    vector _queue;
    int _nums;
    Sem _spaceSem;
    Sem _dataSem;
    int c_step;
    int p_step;
    pthread_mutex_t cLock;
    pthread_mutex_t pLock;
};

sem.hpp

#ifndef _SEM_HPP_
#define _SEM_HPP_

#include 
#include 

class Sem
{
public:
    Sem(int value)
    {
        sem_init(&sem_, 0, value);
    }
    void p()
    {
        sem_wait(&sem_);
    }
    void v()
    {
        sem_post(&sem_);
    }
    ~Sem()
    {
        sem_destroy(&sem_);
    }
private:
    sem_t sem_;
};

#endif

【Linux】信号量(基于环形队列的生产消费模型)_第3张图片


总结

最后有几个特别关键的问题:
1.为什么有了单生产者和单消费者,还要去实现多生产者多消费者呢?不是在访问时都是互斥的吗?

虽然在加锁后只有一个线程可以访问临界区,但是在加锁之前就已经分配信号量了,不要单纯的认为生产者将数据从私有放入公共空间,消费者从公共空间把数据拿到私有空间就完成了生产和消费,其实最耗费时间的过程在任务产生前和拿到任务之后的处理过程,虽然在拿任务时是一个一个拿的,但是处理任务确实并发的。
就有点像区食堂区吃饭,最耗费时间的过程不是买饭,而是去吃饭,多生产多消费的好处就是虽然买饭时是排队的,但是在,买到饭之后,多个人可以一块吃饭,大大提高了效率。

2.信号量本质上是一把计数器,那么计数器的意义是什么呢?

计数器的意义就是,可以不进入临界区,在外部就可以知道临界资源的情况,只要在外部可以申请到信号量,那么就意味着我们一定可以访问临界资源。

我们再来看前边的阻塞队列:
【Linux】信号量(基于环形队列的生产消费模型)_第4张图片
首先先申请锁,在进入临界区之后,再去判断条件是否满足,如果不满足就会被挂起,本质就是在外部并不知道临界资源的情况。
而信号量是对临界资源的预定机制,所以在外部一定可以知道临界区的情况,所以可以减少在临界区的判断过程。

你可能感兴趣的:(linux,运维,服务器)