Linux操作系统7- 线程同步与互斥6(POSIX信号量与环形队列生产者消费者模型)

上篇文章:Linux操作系统7- 线程同步与互斥5(POSIX条件变量生产者消费者模型的进一步使用)-CSDN博客

本篇代码仓库:Linux操作系统学习 - 使用信号量和环形数组实现生产者消费者模型 · e261d75 · 橘子真甜/Linux操作系统与网络编程学习 - Gitee.com

目录

一. 条件变量生产者消费者模型的缺陷

二. 信号量概念及其相关接口⭐

 2.1 什么是信号量,有什么作用?

2.2 POSIX信号量相关接口使用 

        a 信号量定义初始化销毁

b 信号量的PV操作 

三. 环形队生产者消费者模型分析 

3.1 分析环形队列

3.2 分析总结 

四. RingQueue队列代码

4.1 成员变量

4.2 构造析构函数

4.3 信号量PV操作

4.4 push/pop函数

五. RingQueue单生产单消费模型测试

5.1 RingQueue.hpp

5.2 Task.hpp

5.3 MainPC.cpp

5.4 测试结果

a 仅控制生产者生产

b 仅控制消费者消费

 c 不做任何控制

六. 多生产多消费? 


一. 条件变量生产者消费者模型的缺陷

        在阻塞队列BlockQueue中,一个线程去访问临界资源的时候,临界区有没有资源线程是不知道的。所以线程需要加锁保护然后再去访问临界资源,否则会导致数据错误。

        过程为:线程加锁 -> 再检测有无资源 -> 再进行相关操作 -> 再解锁。而加锁解锁是非常消耗时间的。并且还需要解决条件变量虚假唤醒的问题!此时可以使用信号量来改善这个问题

二. 信号量概念及其相关接口⭐

 2.1 什么是信号量,有什么作用?

        信号量是用来衡量一个临界资源中临界资源数量多少的。其实就是一个计数器,用于获取临界资源中资源的数目

         线程在访问临界资源之后可以通过信号量来判断临界资源中是否有合适的资源。这样一来就不需要加锁访问临界区了.

        通过信号量,如果知道了临界区有合适的资源,线程就直接访问临界资源。如果发现临界区没有合适的资源使用时候,就阻塞等待!无需加锁判断

        线程访问临界资源的时候需要申请信号量。离开临界资源时候需要释放信号量。

        那么申请信号量和释放信号量也是临界资源了,需不需要加锁呢?无需加锁,因为申请释放信号量操作就是原子性的!

        int sem = 10

        申请信号量wait(sem),       信号量减少 sem--        ->   表示线程申请临界资源     P操作

        释放信号量signal(sem),    信号量增加 sem++     ->    表示线程归还临界资源     V操作

2.2 POSIX信号量相关接口使用 

        a 信号量定义初始化销毁

//所需头文件
#include <semaphore.h>

//定义
sem_t sem;

//初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);

//参数说明
sem:需要初始化的信号量
pshared:  0表示线程信号量用于线程通信,1表示进程信号量用于进程通信
value:信号量的初始值

//销毁
int sem_destroy(sem_t *sem);

销毁sem这个信号量

b 信号量的PV操作 

//所需头文件
#include <semaphore.h>

//P操作,sem--
int sem_wait(sem_t *sem);
//用于等待信号量,调用成功后sem--,sem为0.会阻塞等待

//V操作,sem++
int sem_post(sem_t *sem);
//用于唤醒信号量,调用成功后sem++,并且唤醒正在等待的信号量



三. 环形队生产者消费者模型分析 

3.1 分析环形队列

        在环形队列中,生产者消费者的下标什么时候会在一起?队列为空或者队列为满的时候。其他时候两个下标根本不会在一起,而是一个追赶的过程!

        所以只有为空和为满的时候才会有互斥的问题。 为空的话,只有生产者才能访问环形队列生成数据,为满的时候只有消费者才能访问环形队列消费数据

3.2 分析总结 

        通过上面的分析可以知道:对于生产者和消费者来说,虽然看到的都是同一个环形队列,资源却是不一样的!

        生产者:环形队列为空的地方都是资源。消费者:环形队列为满的地方都是资源。

假设 producer_sem = 10         那么        consumer_sem = 0

假设 consumer_sem = 10         那么        producer_sem = 0

并且 comsumer_sem + producer_sem = 10

环形队列还需要生产者下标和消费者下标用于访问队列

四. RingQueue队列代码

4.1 成员变量

        根据之前的分析,我们需要生产者消费者信号量与下标。以及一个环形队列。环形队列不可以直接使用queue替代,这里使用vector数组。

#pragma once
#include <iostream>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
const int gnum = 10; //队列的最大容量

template <class T>
class RingQueue
{
public:
    RingQueue(int maxnum = gnum) : _maxnum(maxnum)
    {}

    ~RingQueue()
    {}

private:
private:
    std::vector<T> _ringQueue; // 使用数组来实现环形队列
    size_t _maxnum;            // 环形队列最大容量

    sem_t _spaceSem; // 生产者空间资源信号量
    sem_t _dataSem;  // 消费者数据资源信号量

    int _producerStep; // 生产者下标
    int _consumerStep; // 消费者下标
};

4.2 构造析构函数

        在构造函数中完成信号量的初始化,在析构函数中完成信号量的销毁释放。注意:空间资源最开始是满的,数据资源最开始是空的

#pragma once
#include <iostream>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
const int gnum = 10; // 阻塞队列的最大容量

template <class T>
class RingQueue
{
public:
    RingQueue(const int &maxnum = gnum) : _maxnum(maxnum), _ringQueue(gnum)
    {
        // 在构造函数中完成信号量的初始化
        int n = sem_init(&_spaceSem, 0, gnum);
        if (n < 0)
            std::cout << "spaceSem信号量初始化失败" << std::endl;
        n = sem_init(&_dataSem, 0, 0);
        if (n < 0)
            std::cout << "dataSem信号量初始化失败" << std::endl;

        _producerStep = _consumerStep = 0;
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }

private:
private:
    std::vector<T> _ringQueue; // 使用数组来实现环形队列
    size_t _maxnum;

    sem_t _spaceSem; // 生产者空间资源信号量
    sem_t _dataSem;  // 消费者数据资源信号量

    int _producerStep; // 生产者下标
    int _consumerStep; // 消费者下标
};

4.3 信号量PV操作

        为了方便申请和释放两种信号量,这里使用一个同一的接口封装sem_wait和sem_post方便管理push和pop操作。

        注意需要将PV函数设置为私有,防止外部对象误操作访问

#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
const int gnum = 10; // 阻塞队列的最大容量

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        // P操作,申请信号量sem--
        int n = sem_wait(&sem);
        if (n < 0)
            std::cerr << "P操作失败" << std::endl;
    }

    void V(sem_t &sem)
    {
        // V操作,释放信号量,sem++
        int n = sem_post(&sem);
        if (n < 0)
            std::cerr << "V操作失败" << std::endl;
    }

public:
    RingQueue(const int &maxnum = gnum) : _maxnum(maxnum), _ringQueue(gnum)
    {
        // 在构造函数中完成信号量的初始化
        int n = sem_init(&_spaceSem, 0, gnum);
        if (n < 0)
            std::cerr << "spaceSem信号量初始化失败" << std::endl;
        n = sem_init(&_dataSem, 0, gnum);
        if (n < 0)
            std::cerr << "dataSem信号量初始化失败" << std::endl;

        _producerStep = _consumerStep = 0;
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }

private:
    std::vector<T> _ringQueue; // 使用数组来实现环形队列
    size_t _maxnum;

    sem_t _spaceSem; // 生产者空间资源信号量
    sem_t _dataSem;  // 消费者数据资源信号量

    int _producerStep; // 生产者下标
    int _consumerStep; // 消费者下标
};

4.4 push/pop函数

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

    // 生产者插入数据
    void push(const T &in)
    {
        // 申请空间资源
        P(_spaceSem);

        // 插入数据
        _ringQueue[_producerStep++] = in;
        _producerStep %= _maxnum;

        // 释放一个数据资源
        V(_dataSem);
    }

    // 消费者获取数据
    void pop(T *out)
    {
        // 申请数据资源
        P(_dataSem);

        // 插入数据
        *out = _ringQueue[_consumerStep++];
        _consumerStep %= _maxnum;

        // 释放一个空间资源
        V(_spaceSem);
    }

五. RingQueue单生产单消费模型测试

5.1 RingQueue.hpp

#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
const int gnum = 10; // 阻塞队列的最大容量

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        // P操作,申请信号量sem--
        int n = sem_wait(&sem);
        if (n < 0)
            std::cerr << "P操作失败" << std::endl;
    }

    void V(sem_t &sem)
    {
        // V操作,释放信号量,sem++
        int n = sem_post(&sem);
        if (n < 0)
            std::cerr << "V操作失败" << std::endl;
    }

public:
    RingQueue(const int &maxnum = gnum) : _maxnum(maxnum), _ringQueue(gnum)
    {
        // 在构造函数中完成信号量的初始化
        int n = sem_init(&_spaceSem, 0, gnum);
        if (n < 0)
            std::cerr << "spaceSem信号量初始化失败" << std::endl;
        n = sem_init(&_dataSem, 0, 0);
        if (n < 0)
            std::cerr << "dataSem信号量初始化失败" << std::endl;

        _producerStep = _consumerStep = 0;
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }

    // 生产者插入数据
    void push(const T &in)
    {
        // 申请空间资源
        P(_spaceSem);

        // 插入数据
        _ringQueue[_producerStep++] = in;
        _producerStep %= _maxnum;

        // 释放一个数据资源
        V(_dataSem);
    }

    // 消费者获取数据
    void pop(T *out)
    {
        // 申请数据资源
        P(_dataSem);

        // 插入数据
        *out = _ringQueue[_consumerStep++];
        _consumerStep %= _maxnum;

        // 释放一个空间资源
        V(_spaceSem);
    }

private:
    std::vector<T> _ringQueue; // 使用数组来实现环形队列
    size_t _maxnum;

    sem_t _spaceSem; // 生产者空间资源信号量
    sem_t _dataSem;  // 消费者数据资源信号量

    int _producerStep; // 生产者下标
    int _consumerStep; // 消费者下标
};

5.2 Task.hpp

        直接沿用BlockQueue队列的任务即可,因为我们做到了解耦!

#pragma once
#include <iostream>
#include <functional>

class CalTask
{
    // 使用c++11 using 和 包装器
    using func_t = std::function<int(int, int, char)>;
    // 当然也可以使用函数指针
    // typedef int (*func_t)(int, int, char);

public:
    CalTask() {}
    CalTask(int x, int y, char op, func_t func)
        : _x(x), _y(y), _op(op), _callback(func) {}
    ~CalTask() {}

    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof(buffer) - 1, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _callback; // 通过回调函数调用计算
};

// 计算函数
int my_math(int x, int y, char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if (y == 0)
        {
            std::cerr << "div zero" << std::endl;
            return -1;
        }
        else
        {
            result = x / y;
        }
        break;
    }
    case '%':
        if (y == 0)
        {
            std::cerr << "moved zero" << std::endl;
            return -1;
        }
        else
        {
            result = x % y;
        }
        break;
    default:
        break;
    }
    return result;
}

5.3 MainPC.cpp

        同样,由于我们做到了解耦,只需要更改少量代码即可!

#include <iostream>
#include <memory>
#include <string>

#include <unistd.h>
#include <pthread.h>
#include "RingQueue.hpp"
#include "Task.hpp"

const std::string OP = "+-*/%";
void *producer(void *args)
{
    // 获取交易场所 - 阻塞队列
    RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);
    while (true)
    {
        int x = rand() % 100;
        int y = rand() % 100;
        char op = OP[rand() % OP.size()];
        // 打印日志
        printf("生产者生产的数据:%d %c %d 并交给消费者计算\n", x, op, y);
        CalTask ct(x, y, op, my_math);
        rq->push(ct);
        sleep(1);
    }

    return nullptr;
}

void *consumer(void *args)
{
    // 获取交易场所 - 生产消费阻塞队列,消费保存阻塞队列
    RingQueue<CalTask> *rq = static_cast<RingQueue<CalTask> *>(args);

    while (true)
    {
        // 获取任务计算
        CalTask ct;
        rq->pop(&ct);
        std::string result = ct();
        std::cout << "消费者获取数据并计算:" << result << std::endl;
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(0) ^ getpid());
    // 建立任务队列和保存队列
    RingQueue<CalTask> *rq = new RingQueue<CalTask>;

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void *)rq);

    pthread_create(&p, nullptr, producer, (void *)rq);

    pthread_join(c, nullptr);

    pthread_join(p, nullptr);
    delete rq;
    return 0;
}

5.4 测试结果

a 仅控制生产者生产

        编译运行结果如下:

可以看到,成功的实现了生产者生产数据和消费者消费数据,并且不会相互冲突! 

b 仅控制消费者消费

        让消费者每次运行sleep(1)

 

 c 不做任何控制

六. 多生产多消费? 

        本模型实现了单个生产者和单个消费者之间的同步!

        下篇文章我会分析多生产多消费中的线程安全问题,如何保证多个生产者之间的同步?多个消费者之间的同步?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子真甜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值