Linux - 生产者消费者模型(信号量使用)

生产者消费者概念

上面是生产者消费者?

生产数据的一方称为生产者, 将数据拿走用于处理的称为消费者.
生产者将产生的数据放入一个固定的区域, 而消费者只需要从这个固定区域取出数据即可

这个存放数据的区域可以当作是超市. 

厂家生产出产品后, 将产品送至超市. (厂家 == 生产者)

我们 (顾客), 则是去到超市购买我们需要的产品. (顾客 == 消费者)

那么生产者消费者模型有什么优点?

生产者和消费者之间通过共享缓冲区间接通信, 而不是直接调用对方的方法. 这种解耦使得生产者和消费者可以独立运行, 互不干扰, 降低了代码的复杂性和耦合性.

生产者只需要生产数据, 放入仓库中, 消费者只需要从仓库中取出数据即可. (降低耦合性)

生产者和消费者可以并行运行, 充分利用多核处理器的性能. 通过缓冲区, 消费者不必直接等待生产者生产数据, 然后进行处理, 只需要去仓库获取数据, 那么生产者生产数据时, 消费者也能同时消费数据. (提高效率)

生产者消费者实现

版本一

这里使用队列作为存储数据的仓库. 通过互斥锁, 条件变量来调节生产者与消费者之间的同步互斥.

template<class T>
class ProducerConsumerBlock
{
public:
    ProducerConsumerBlock(int capacity = 5)
    :_capacity(capacity),
    _size(0)
    {
        pthread_mutex_init(&_mutex, NULL);
        pthread_cond_init(&_isempty, NULL);
        pthread_cond_init(&_isfull, NULL);
    }

    // 生产者放入数据
    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
        while(_size == _capacity) // 当仓库已经满了, 那么就让生产者加入休眠状态
        {                         // 使用 while 循环, 被唤醒后, 再次检查是否有空间, 安全性更高
            pthread_cond_wait(&_isfull, &_mutex);
        }

        _qe.push(data); // 将数据放入队列中
        ++_size;

        if(_size > _capacity / 2) // 当数据数量超过容量的一半, 就唤醒消费者
        {
            pthread_cond_broadcast(&_isempty); // 可以只唤醒一个, 也可以一次性全部唤醒
        }

        pthread_mutex_unlock(&_mutex);

    }

    void pop(T* out)
    {
        pthread_mutex_lock(&_mutex);

        while(_size == 0) // 检测是否有数据, 没有数据那么就进行等待, 等待生产者唤醒消费者
        {
            pthread_cond_wait(&_isempty, &_mutex);
        }

        *out = _qe.front(); // 取出数据
        _qe.pop();
        --_size;

        if(_size < _capacity / 2) // 当数据的数量小于容量的一半, 那么唤醒生产者
        {
            pthread_cond_broadcast(&_isfull);
        }

        pthread_mutex_unlock(&_mutex);
    }

    ~ProducerConsumerBlock()
    {}
private:
    std::queue<T> _qe; // 存放数据的仓库
    pthread_mutex_t _mutex; // 互斥访问仓库
    int _size; // 当前已经有多少数据存放于仓库
    int _capacity; // 仓库的最大容量
    pthread_cond_t _isempty; // 仓库是否满了
    pthread_cond_t _isfull; // 仓库是否空了
};

版本二

这个版本我们需要使用信号量来完成.

在上面的代码中, 我们通过一个互斥锁, 限制所有的生产者和消费者访问仓库. 但实际上, 生产者只关系是否还有空间, 消费者只关系是否还有数据, 它们之间获取的资源不相同, 所以直接用互斥锁来同时限制生产者和消费者是效率比较低的. 这里可以使用信号量.

信号量: 本质上就是一个计数器, 可以用来记录资源的数量. 当线程需要什么资源时, 就需要先将这个计数器减一 (计数器不能小于 0), 如果减一成功, 那么获取资源就成功了. 如减一不成功, 那么就也会进行等待, 直到减一成功 (申请资源成功).

sem: _space(5) 剩余空间资源, 5 表示初始化大小为 5, 即刚开始空间资源为 5 份

sem: _data(0) 剩余数据资源, 0 表示初始化大小0, 刚开始数据资源为 0 .

生产者申请 _space, 申请成功, 那么生产者就继续向下执行, 否则进行等待

消费者则申请 _data, 申请成功, 那么消费者就继续向下执行, 否则进行等待

信号量的使用

1. 初始化信号量

使用函数: int sem_init(sem_t* sem, int pshared, unsigned int value);

pshared: 0 表示线程间共享, 非零表示进程间共享

value: 信号量初始值

sem_t _space_sem;
sem_init(&_space_sem, 0, 5);

2. 申请资源

使用函数: int sem_wait(sem_t* sem)

使用之后, sem 的值就会减一 (不能小于 0)

sem_wait(&_space_sem);

3. 释放资源

使用函数: int sem_post(sem_t* sem)

使用之后, sem 的值就会加一

sem_post(&_space_sem);

4. 销毁信号量

使用函数: int sem_destroy(semt_t* sem)

sem_destroy(&_space_sem);

总的来说信号量是比较简单的. 用法上和条件变量是差不多的, 只不过条件变量大小固定就是1, 而信号量的大小可以由自己控制.

代码实现

这里用于存放数据的仓库不再使用队列, 这里使用环形队列.

template<class T>
class ProducerConsumer
{
public:
    ProducerConsumer(int n = 5)
    :_data(n),
    _push_index(0),
    _pop_index(0),
    _size(0)
    {
        pthread_mutex_init(&_producer, NULL);
        pthread_mutex_init(&_consumer, NULL);
        sem_init(&_space_sem, 0, n);
        sem_init(&_data_sem, 0, 0);
    }

    void push(const T& data)
    {
        sem_wait(&_space_sem); // 申请空间资源
        pthread_mutex_lock(&_producer); // 申请资源成功, 对仓库进行加锁, 放入数据
        _data[_push_index] = data;
        _push_index = (_push_index + 1) % _data.size();
        _size++;
        pthread_mutex_unlock(&_producer);
        sem_post(&_data_sem); // 增加数据资源
    }

    void pop(T* out)
    {
        sem_wait(&_data_sem); // 申请数据资源
        pthread_mutex_lock(&_consumer); // 获取数据
        *out = _data[_pop_index];
        _pop_index = (_pop_index + 1) % _data.size();
        _size--;
        pthread_mutex_unlock(&_consumer);
        sem_post(&_space_sem); // 增加空间资源
    }

    ~ProducerConsumer()
    {
        pthread_mutex_destroy(&_producer);
        pthread_mutex_destroy(&_consumer);
        sem_destroy(&_space_sem);
        sem_destroy(&_data_sem);
    }
private:
    std::vector<T> _data; // 存放数据的仓库
    int _push_index; // 放入数据的位置 (下标)
    int _pop_index; // 取出哪个位置的数据 (下标)
    int _size; // 当前已经存放了多少数据
    sem_t _space_sem; // 代表剩余空间资源的信号量
    sem_t _data_sem; // 代表剩余数据资源的信号量
    pthread_mutex_t _producer; // 生产者之间的互斥
    pthread_mutex_t _consumer; // 消费者之间的互斥
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值