【Linux】POSIX信号量、环形队列、基于环形队列实现生产者消费者模型

1.信号量

1.1 相关概念

信号量(sem)也叫信号灯,类型是sem_t ,就是一个计数器 ,用来表明临界资源的资源数量 ,比如说看电影的时候,电影票就是资源,我们只要买了票这个资源就是自己的,所以信号量的本质就是对特定资源的预定机制

资源为1 的信号量叫二元信号量,二元信号量的本质就是互斥。

多线程使用资源有两种场景:

  1. 将目标资源整体使用(要用到锁mutex/二元信号量),我们之前实现的阻塞队列,就是把阻塞队列整体使用,就要加锁
  2. 将目标资源按不同的"块"分批使用(要用到信号量),今天要介绍的环形队列

所有线程申请信号量,就都要看到信号量,所以信号量也是临界资源。

信号量就是一个计数器,申请信号量就是对这个计数器--(P 操作),释放资源就是对计数器++(V 操作),PV操作都是原子的

  • 初始化:pshared: 0表⽰线程间共享,⾮零表⽰进程间共享;value:信号量初始值

    cpp 复制代码
    #include <semaphore.h>
    int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 销毁信号量:

    cpp 复制代码
    int sem_destroy(sem_t *sem);
  • 等待信号量:P操作,会将信号量的值减1

    cpp 复制代码
    int sem_wait(sem_t *sem); //P()
  • 发布信号量:V操作,表⽰资源使⽤完毕,可以归还资源了,将信号量值加1

    cpp 复制代码
    int sem_post(sem_t *sem);//V()

1.2 封装信号量

cpp 复制代码
//Sem.hpp文件
#pragma once
#include <iostream>
#include <semaphore.h>

namespace MySem
{
    const unsigned int default_sem_value = 1;
    class Sem
    {
    public:
        Sem(unsigned int sem_value = default_sem_value)
        {
            sem_init(&_sem, 0, sem_value);
        }

        void P() // 原子的
        {
            int n = sem_wait(&_sem);
            (void)n;
        }

        void V() // 原子的
        {
            int n = sem_post(&_sem);
            (void)n;
        }

        ~Sem()
        {
            sem_destroy(&_sem);
        }

    private:
        sem_t _sem;
    };
}

2.环形队列

环形结构起始状态和结束状态都是⼀样的,不好判断为空或者为满,所以可以通过加计数器或者
标记位来判断满或者空,另外也可以预留⼀个空的位置,作为满的状态。

环形队列采⽤固定长度的 数组模拟 ,⽤ 模运算来模拟环状特性。

现在我们的环形队列每一个位置都能放数据,为空或为满的情况通过 信号量判断。

现在要求一个生产者往这个队列里放数据,一个消费者从这个队列里拿数据。生产者不能把消费者套一个圈,消费者不能超过生产者。

  • 队列为空或为满时:生产者和消费者指向同一个位置
  • 队列为空时:消费者没东西可拿,所以要求只能 (互斥 实现)让生产者 (同步实现)运行,
  • 队列为满时:生产者没位置再生产,所以要求只能 (互斥 实现)让消费者 (同步实现)运行。
  • 不为空也不为满时:消费者和生产者就能同时运行

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

3.1 单生产单消费

所以对生产者 来说,有用的资源是空位置对消费者 来说,有用的资源是数据,初始状态下,队列有多大,生产者的信号量就有多少,消费者此时没有资源,信号量就是0。

cpp 复制代码
#pragma once

#include "Sem.hpp"  //用我们前面封装好的的信号量
#include <iostream>
#include <string>
#include <vector>

using namespace MySem;

const int defualt_cap = 1;

template <typename T>
class CirQueue
{
public:
    CirQueue(int cap = defualt_cap)
        : _cq(cap),
          _cap(cap),
          _producer_sem(cap),
          _p_index(0),
          _consumer_sem(0),
          _c_index(0)
    {
    }


    ~CirQueue()
    {
    }

private:
    std::vector<T> _cq;
    int _cap; 

    Sem _producer_sem; // 生产者信号量
    Sem _consumer_sem; // 消费者信号量

    int _p_index; // 生产者下标
    int _c_index; // 消费者下标
};

对于生产者,生产之前要申请信号量(P操作,就是-1),生产完后,要释放消费者信号量(V操作,就是+1)。

cpp 复制代码
    void Equeue(const T &data) // 生产者
    {
        _producer_sem.P();      // 申请信号量
        _cq[_p_index++] = data; // 生产,生产完了要往后走
        _p_index %= _cap;       // 维持环状结构
        _consumer_sem.V();      // 让消费者的信号量+1
    }

对于消费者,消费之前要申请信号量,生产完后,要释放生产者的信号量。

cpp 复制代码
    T Pop() // 消费者
    {
        _consumer_sem.P();        // 申请信号量
        T data = _cq[_c_index++]; // 获取数据,获取之后往后走
        _c_index %= _cap;         // 维持环状结构
        _producer_sem.V();        // 让生产者信号量+1
        return data;
    }

验证一下。

cpp 复制代码
//Main.cc文件
#include "CircularQueue.hpp"
#include <unistd.h>

void *PThreadFunc(void *arg) // 生产者
{
    int data = 1;
    CirQueue<int> *cq = static_cast<CirQueue<int> *>(arg);
    while (true)
    {
        std::cout << "生产了一个数据: " << data<< std::endl;
        cq->Equeue(data);
        data++;
        sleep(1);
    }
    return nullptr;
}

void *CThreadFunc(void *arg) // 消费者
{
    CirQueue<int> *cq = static_cast<CirQueue<int> *>(arg);
    while (true)
    {
        int data = cq->Pop();
        std::cout << "消费了一个数据: " << data << std::endl;
    }
    return nullptr;
}

int main()
{
    CirQueue<int> cq(5); // 环形队列
    pthread_t p1, c1;

    pthread_create(&p1, nullptr, PThreadFunc, &cq);
    pthread_create(&c1, nullptr, CThreadFunc, &cq);

    pthread_join(p1, nullptr);
    pthread_join(c1, nullptr);

    return 0;
}

这是消费者比生产者快的情况,生产者生产一个,消费者就消费一个。

还可以让消费者比生产者慢,就会出现生产者一次性把队列干满,然后消费者消费一个生产者就生产一个。

3.2 多生产多消费

多生产多消费就要加锁来维护生产者与生产者之间的关系,以及消费者与消费者之间的关系,所以我们要两把锁 。锁用之前我们封装过的,在【Linux】线程的互斥 里有详细介绍。

cpp 复制代码
    void Equeue(const T &data) // 生产者
    {
        //_producer_mutex.Lock(); //之前加锁?
        _producer_sem.P(); // 申请信号量
        //_producer_mutex.Lock(); //之后加锁?

        _cq[_p_index++] = data; // 生产,生产完了要往后走
        _p_index %= _cap;       // 维持环状结构

        //_producer_mutex.UnLock(); //之前解锁?
        _consumer_sem.V(); // 让消费者的信号量+1
        //_producer_mutex.UnLock(); //之后解锁?
    }

加锁的操作是在申请信号量之前还是之后呢?

肯定有很多同学认为,信号量也是临界资源,肯定要放在锁里面,也就是先加锁再申请信号量。事实并非如此。

可以理解为加锁就是我们在电影院门口排队,申请信号量就是在手机上买票,我们是先排着队再票效率高,还是先把票买了再去排队效率高?肯定是先买票。

对信号量的申请本质就是对资源的预定机制,而且申请信号量本身就是原子的,所有的线程先把信号量资源瓜分了,然后在串行式的申请锁,我申请到锁进入临界区后,别的线程可以去申请信号量,这样效率更高;反之,申请到锁的线程再去申请信号量,别的线程还要等这个线程把锁释放了才能申请信号量。

所以我们应该先申请信号量,再加锁

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"

using namespace MySem;
using namespace MyMutex;

const int defualt_cap = 1;
template <typename T>
class CirQueue
{
public:
    CirQueue(int cap = defualt_cap)
        : _cq(cap),
          _cap(cap),
          _producer_sem(cap), // 空位的数量
          _p_index(0),
          _consumer_sem(0), // 数据的数量
          _c_index(0)
    {
    }

    void Equeue(const T &data) // 生产者
    {
        _producer_sem.P(); // 申请信号量
        {
            LockGuard lg(&_producer_mutex); // 加锁
            _cq[_p_index++] = data; // 生产,生产完了要往后走
            _p_index %= _cap;       // 维持环状结构
        }
        _consumer_sem.V(); // 让消费者的信号量+1
    }

    T Pop() // 消费者
    {
        T data;
        _consumer_sem.P(); // 申请信号量
        {
            LockGuard lg(&_consumer_mutex); // 加锁
            data = _cq[_c_index++]; // 获取数据,获取之后往后走
            _c_index %= _cap;       // 维持环状结构
        }
        _producer_sem.V(); // 让生产者信号量+1
        return data;
    }

    ~CirQueue()
    {
    }

private:
    std::vector<T> _cq;
    int _cap;

    Sem _producer_sem; // 生产者信号量
    Sem _consumer_sem; // 消费者信号量

    int _p_index; // 生产者下标
    int _c_index; // 消费者下标

    Mutex _producer_mutex; // 生产者的互斥锁
    Mutex _consumer_mutex; // 消费者的互斥锁
};

如果现在环形队列的大小为1,就意味着环形队列被当作一个整体使用了,也就变成了互斥锁,也就是之前说过的阻塞队列。

本片分享就到这里,我们下篇见~

相关推荐
hui函数2 小时前
python全栈(基础篇)——day04:后端内容(字符编码+list与tuple+条件判断+实战演示+每日一题)
开发语言·数据结构·python·全栈
Never_Satisfied2 小时前
在JavaScript / HTML中,转移字符导致js生成的html出错
开发语言·javascript·html
数据知道3 小时前
Go语言:用Go操作SQLite详解
开发语言·后端·golang·sqlite·go语言
晨非辰4 小时前
《剑指Offer:单链表操作入门——从“头删”开始破解面试》
c语言·开发语言·数据结构·c++·笔记·算法·面试
sheji34168 小时前
【开题答辩全过程】以 python杭州亚运会数据分析与可视化开题为例,包含答辩的问题和答案
开发语言·python·数据分析
代码萌新知8 小时前
设计模式学习(五)装饰者模式、桥接模式、外观模式
java·学习·设计模式·桥接模式·装饰器模式·外观模式
iナナ11 小时前
Spring Web MVC入门
java·前端·网络·后端·spring·mvc
驱动探索者11 小时前
find 命令使用介绍
java·linux·运维·服务器·前端·学习·microsoft
卷Java11 小时前
违规通知功能修改说明
java·数据库·微信小程序·uni-app