【Linux系统】生产者消费者模型

生产者消费者模型

  • 321原则(便于记忆)
    • 三种关系
      1. 生产< - > 生产:互斥
      2. 生产< - > 生产:互斥
      3. 生产< - > 消费:互斥,同步
    • 两种角色:
      1. 生产者
      2. 消费者
    • 一个交易场所 (仓库,共享临界资源)

我们要研究生产者消费者模型,本质上就是要搞清多个生产消费之间的互斥同步关系

基本介绍

生产者和消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型优点

  • 解耦
  • 支持并发
  • 支持忙闲不均
    在这里插入图片描述

基于BlockingQueue的生产者消费者模型

BlockingQueue

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。

其与普通的队列区别在于:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
  • 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
  • 注:以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞。
    在这里插入图片描述

C++ queue模拟阻塞队列的生产消费模型

代码说明

  • 以单生产者,单消费者,来进行说明
  • 采用原始接口。

BlockQueue.hpp

#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__

#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>

template <typename T>
class BlockQueue {
private:
    bool IsFull() {
        return _block_queue.size() == _cap;
    }
    bool IsEmpty() {
        return _block_queue.empty();
    }

public:
    BlockQueue(int cap) : _cap(cap) {
        _producer_wait_num = 0;
        _consumer_wait_num = 0;
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_product_cond, nullptr);
        pthread_cond_init(&_consum_cond, nullptr);
    }
    void Enqueue(T &in) {  // 生产者用的接口
        pthread_mutex_lock(&_mutex);
        while (IsFull()) {  // 保证代码的健壮性
            // 生产线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!
            // 1. pthread_cond_wait调用是:a. 让调用线程等待;b. 自动释放曾经持有的_mutex锁;c. 当条件满足,线程唤醒,pthread_cond_wait要求线程:必须重新竞争mutex锁,竞争成功,方可返回!!!
            _producer_wait_num++;
            pthread_cond_wait(&_product_cond, &_mutex);  // 只要等待,必定会有唤醒,唤醒的时候,就要继续从这个位置往下走!!
            _producer_wait_num--;
            // 之后:安全
        }
        // 进行生产
        _block_queue.push(std::move(in));
        // 通知消费者来消费
        if (_consumer_wait_num > 0)
            pthread_cond_signal(&_consum_cond);
        pthread_mutex_unlock(&_mutex);
    }
    void Pop(T *out) {  // 消费者用的接口
        pthread_mutex_lock(&_mutex);
        while (IsEmpty()) {  // 保证代码的健壮性
            // 消费线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!
            // 1. pthread_cond_wait调用是:a. 让调用线程等待;b. 自动释放曾经持有的_mutex锁;c. 当条件满足,线程唤醒,pthread_cond_wait要求线程:必须重新竞争mutex锁,竞争成功,方可返回!!!
            _consumer_wait_num++;
            pthread_cond_wait(&_consum_cond, &_mutex);  // 伪唤醒
            _consumer_wait_num--;
        }
        // 进行消费
        *out = _block_queue.front();
        _block_queue.pop();
        // 通知生产者来生产
        if (_producer_wait_num > 0)
            pthread_cond_signal(&_product_cond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_product_cond);
        pthread_cond_destroy(&_consum_cond);
    }

private:
    std::queue<T> _block_queue;  // 阻塞队列,是被整体使用的!!!
    int _cap;                    // 上限
    
    pthread_mutex_t _mutex;      // 保护_block_queue的锁
    
    pthread_cond_t _product_cond; // 专门给生产者提供的条件变量
    pthread_cond_t _consum_cond;  // 专门给消费者提供的条件变量
    
    int _producer_wait_num;
    int _consumer_wait_num;
};

#endif

注意:这里采用模板,是想告诉我们,队列中不仅仅可以防止内置类型,比如int,对象也可以作为任务来参与生产消费的过程哦。

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

// 任务类型1
// class Task
// {
// public:
//     Task() {}
//     Task(int a, int b) : _a(a), _b(b), _result(0)
//     {
//     }

//     void Execute()
//     {
//         _result = _a + _b;
//     }
//     std::string ResultToString()
//     {
//         return std::to_string(_a) + "+" + std::to_string(_b) + "=" + std::to_string(_result);
//     }
//     std::string DebugToString()
//     {
//         return std::to_string(_a) + "+" + std::to_string(_b) + "=?";
//     }

// private:
//     int _a;
//     int _b;
//     int _result;
// };

// 任务类型2
using Task = std::function<void()>;

为什么 pthread_cond_wait 需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

  • 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_mutex_unlock(&mutex);
    //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
  • 由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait。所以解锁和等待必须是一个原子操作。
  • int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); 功能特性:
    • 自动释放锁:调用 pthread_cond_wait 时,线程会自动释放 mutex 所指向的互斥锁,然后进入阻塞状态,等待其他线程通过 pthread_cond_signalpthread_cond_broadcast 函数来唤醒它。
    • 重新获取锁:当线程被唤醒后,pthread_cond_wait 函数会尝试重新获取 mutex 所指向的互斥锁。只有重新获取锁成功,该函数才会返回,线程继续执行后续代码。

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

POSIX信号量

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

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

  • pshared: 0表示线程间共享,非零表示进程间共享
  • value: 信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

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

发布信号量

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

上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):

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

  • 环形队列采用数组模拟,用模运算来模拟环状特性
    在这里插入图片描述

但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。
在这里插入图片描述

#pragma once
#include <iostream>
#include <semaphore.h>


class Sem
{
public:
    Sem(int n)
    {
        sem_init(&_sem, 0, n);
    }

    void P()
    {
        sem_wait(&_sem);
    }
    void V()
    {
        sem_post(&_sem);
    }
    ~Sem()
    {
        sem_destroy(&_sem);
    }
private:
    sem_t _sem;
};
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <semaphore.h>
#include <pthread.h>

// 单生产,单消费
// "多生产,多消费"
// "321":
// a: 生产和消费互斥和同步
// b: 生产者之间:
// c: 消费者之间:
// 解决方案: 加锁
// 1. 需要几把锁?2把
// 2. 如何加锁?

template<typename T>
class RingQueue
{
private:
    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    void Unlock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);
    }
public:
    RingQueue(int cap)
        :_cap(cap),
         _room_sem(cap),
         _data_sem(0),
         _producer_step(0),
         _consumer_step(0)
    {
        pthread_mutex_init(&_producer_mutex, nullptr);
        pthread_mutex_init(&_consumer_mutex, nullptr);
    }
    void Enqueue(const T &in)
    {
        // 生产行为
        _room_sem.P();
        Lock(_producer_mutex);
        // 一定有空间!!!
        _ring_queue[_producer_step++] = in; // 生产
        _producer_step %= _cap;
        Unlock(_producer_mutex);
        _data_sem.V();
    }
    void Pop(T *out)
    {
        // 消费行为
        _data_sem.P();
        Lock(_consumer_mutex);
        *out = _ring_queue[_consumer_step++];
        _consumer_step %= _cap;
        Unlock(_consumer_mutex);
        _room_sem.V();
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&_producer_mutex);
        pthread_mutex_destroy(&_consumer_mutex);
    }
private:
    // 1. 环形队列
    std::vector<T> _ring_queue;
    int _cap; // 环形队列的容量上限
    // 2. 生产和消费的下标
    int _producer_step;
    int _consumer_step;
    // 3. 定义信号量
    Sem _room_sem; // 生产者关心
    Sem _data_sem; // 消费者关心
    // 4. 定义锁,维护多生产多消费之间的互斥关系
    pthread_mutex_t _producer_mutex;
    pthread_mutex_t _consumer_mutex;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值