生产者消费者模式(并发型)

生产者消费者模式 (Producer-Consumer Pattern) 是一种并发编程模式,用于解决生产者和消费者之间数据交换的问题。在这种模式中,生产者生产数据,并将其传递给消费者进行处理。生产者和消费者是独立的进程或线程,它们通过共享的缓冲区进行通信

相关的一些概念:

  • 生产者:负责生成数据并将其放入缓冲区。
  • 消费者:负责从缓冲区取出数据并进行处理。
  • 缓冲区:缓冲区是一个固定大小的存储区,用来在生产者和消费者之间传递数据。缓冲区可以是一个简单的数组、队列或者任何其他适合的数据结构。(本次实现中采用的是队列)

下面有点绕这一部分,不行就gpt辅助理解:

信号量(Semaphore)和条件变量(Condition Variable)在多线程编程中都是用于同步和协调线程的工具,但它们在概念和使用上有一些区别。

信号量

生产者 - 消费者模型中的信号量通常用来计数资源的数量,分为计数信号量(Counting Semaphore)和二进制信号量(Binary Semaphore)两种类型。计数信号量 可以取任意非负值,用于控制对多个资源的访问。二进制信号量 只有0和1两个值,用于实现互斥锁(Mutex),控制对单个资源的访问。

在生产者-消费者模型中,通常使用两个信号量来管理缓冲区的状态:

  • PV操作

    • P 操作(Proberen):wait(),主要使用资源,如果资源不可用,进程会被阻塞,直到资源可用。P 操作会将信号量的值减一
    • V 操作(Verhogen):signal(),主要释放资源,进而允许其他等待该资源的进程继续执行。V 操作会将信号量的值加一,表示增加一个可用资源
    • PV操作通常是成对出现
  • 示例说明

    假设我们有一个缓冲区,其大小为 BUFFER_SIZE = 5,初始状态如下:

    • 空位信号量(empty)初始值为5。
    • 满位信号量(full)初始值为0。

生产者-消费者操作示例

  1. 生产者操作
    • 生产者在向缓冲区放入一个元素之前,会等待空位信号量(empty)大于0。
    • 向缓冲区放入一个元素后,空位信号量减1,满位信号量加1。
  2. 消费者操作
    • 消费者在从缓冲区取出一个元素之前,会等待满位信号量(full)大于0。
    • 从缓冲区取出一个元素后,满位信号量减1,空位信号量加1。

信号量的变化反映了缓冲区的使用情况,但信号量本身并不是缓冲区的大小。信号量用于协调生产者和消费者对缓冲区的访问,确保线程同步和数据完整性。

条件变量

条件变量(Condition Variable)用于让线程等待某个条件成立,并在该条件可能改变时被其他线程通知。条件变量本身没有特殊的数值属性,使用等待和通知机制来协调多个线程的运行,使线程能够在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用,可以有效地解决复杂的线程同步问题,确保线程安全地访问共享资源。

  • 条件变量适用于需要在某个条件满足时唤醒一个或多个线程的情况。常用于复杂的同步场景,例如生产者-消费者模型中,当缓冲区为空或满时进行等待和通知。
  • 相关操作
    • wait (有时还包括 timed_wait):线程等待条件变量,并自动释放关联的互斥锁。当被唤醒时,线程重新获取互斥锁。
    • signal (或 notify_one):唤醒等待在条件变量上的一个线程。
    • broadcast (或 notify_all):唤醒等待在条件变量上的所有线程。

生产者-消费者操作示例

  1. 生产者线程

    • 生产一个数据项。

    • 获取互斥锁。

    • 如果缓冲区满,调用 pthread_cond_wait(&cond_empty, &mutex),等待空位出现,并释放互斥锁。被唤醒后,重新获取互斥锁。

    • 将数据放入缓冲区。

    • 通过 pthread_cond_signal(&cond_full) 通知等待在 cond_full 条件变量上的消费者。

    • 释放互斥锁。

  2. 消费者线程

    • 获取互斥锁。
    • 如果缓冲区为空,调用 pthread_cond_wait(&cond_full, &mutex),等待数据出现,并释放互斥锁。被唤醒后,重新获取互斥锁。
    • 从缓冲区取出数据。
    • 通过 pthread_cond_signal(&cond_empty) 通知等待在 cond_empty 条件变量上的生产者。
    • 释放互斥锁。
    • 消费数据。

小总结

  • 信号量 更适合简单的计数资源同步,直接控制资源的可用性。
  • 条件变量 更适合复杂的条件同步,需要在特定条件下阻塞和唤醒线程。

生产者-消费者模型特点

  • 保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据
  • 当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒
  • 优点:解耦合、复用、调整并发数、异步、支持分布式

在这里插入图片描述

下面开始实现:使用阻塞队列

由于C++实现生产者-消费者模型需要依赖:C++11 提供的 thread 库;互斥锁 mutex;条件变量 condition_variable;队列 queue;因此需要引入相关的头文件

先写一个简单的main函数:

#include <thread>
#include <condition_variable>
#include <mutex>
#include <queue>

#include <cstdio>

class BlockQueue{
    int size_maxnum;  //缓冲区大小,缓冲区中可以存储的最大元素数,这里是vec的最大容量
    std::queue<int> vec;
    
};

void proceducer() {
    printf("pro init \n");

}

void consumer() {
    printf("con init \n");

}

int main() {

    std::thread tp(proceducer);  // 创建一个生产者线程;
    std::thread tc(consumer);  // 创建一个消费者线程

    tp.join();  // 等待线程完成执行
    tc.join();

    printf("hello world \n");  // 主线程会等待这两个线程都完成后才继续执行。
    return 0;
}

编译后报错: note: ‘std::thread’ is defined in header ‘’; did you forget to ‘#include ’?

检查发现头文件已经包含了啊,查资料:Windows上无法使用thread头文件_thread’ was not declared in this scope-CSDN博客

查看使用的编译器:

g++ -v:gcc version 8.1.0 (x86_64-win32-seh-rev0, Built by MinGW-W64 project)

于是换一个这样式儿的:gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.3)

又报错。。。/tmp/ccU4iMen.o: in function std::thread::thread<void (&)(), , void>(void (&)())': produtor_sumer.cpp:(.text._ZNSt6threadC2IRFvvEJEvEEOT_DpOT0_[_ZNSt6threadC5IRFvvEJEvEEOT_DpOT0_]+0x33): undefined reference to pthread_create’

gpt给出解决方法:链接错误提示是由于缺少对pthread库的链接。在使用std::thread时,必须链接到pthread库。你可以通过在编译时添加-lpthread选项来解决这个问题。

g++ ProceducerConsumer.cpp -l pthread,终于通过了。。

写一个阻塞队列类:

template <typename T>
class BlockQueue{

    // 数据类型使用模板指定,与消费者取出的类型一样
    std::queue<T> vec;

    std::condition_variable m_not_Empty;  // 消费者线程条件变量
    std::condition_variable m_not_Full;  // 生产者线程条件变量
    std::mutex m_mtx;  // 互斥锁
    int size_maxnum;  //缓冲区大小,缓冲区中可以存储的最大元素数,这里是vec的最大容量

public:
    BlockQueue(int capacity = 10): size_maxnum(capacity) {}

    T pop(int id) {  // 消费者,意为从vector中pop数据
        std::unique_lock<std::mutex> lock(m_mtx);

        while(vec.empty()) {  // 当容量为空时等待
            m_not_Empty.wait(lock);
        }
        // m_not_Empty.wait(lock, [this]{ return !vec.empty(); });  // lambda表达式等待 

        T val = vec.front();
        vec.pop();
        printf("Queue size after pop: %lu, C-thread[%d]==>Consumed: %d\n", vec.size(), id, val);
        m_not_Full.notify_all();  // 解锁,唤醒生产者线程
        return val;
    }

    void push(const T& val, int id) {  // 生产者,意为向vector中push数据
        std::unique_lock<std::mutex> lock(m_mtx);  // 创建了一个智能锁对象 lock,将互斥量 mtx 锁住。
        
        while(vec.size() >= size_maxnum) {  // 当队列已满时等待
            m_not_Full.wait(lock);
        }
        // m_not_Full.wait(lock, [this]{ return vec.size() < size_maxnum; });

        vec.push(val);
        printf("Queue size after push: %lu, P-thread[%d]==>Produced: %d\n", vec.size(), id, val);
        m_not_Empty.notify_all();  // 解锁,唤醒消费者线程
    }
    
};

条件变量 m_not_Emptym_not_Full 用于管理队列的状态变化,从而确保生产者不会在队列满时继续添加数据,消费者不会在队列空时尝试取数据。这样的实现既保证了线程安全,又提高了资源利用率。

定义生产者和消费者的操作:

void producer(BlockQueue<int>* q, int id) {
    for (int i = 0; i < 5; ++i) {
        q->push(i + id * 100, id);  // 每个线程生产的数据都不同,便于调试
    }
}

void consumer(BlockQueue<int>* q, int id) {
    for (int i = 0; i < 5; ++i) {
        int item = q->pop(id);
    }
}

两种测试代码:上面是一个生产者,一个消费者;下面是多个生产和多个消费,均可检查阻塞队列的正确性

int main() {

    BlockQueue<int> q(2);  // 阻塞队列的初始容量为2

    std::thread tp(producer, &q, 1);  // 创建一个生产者线程;队列对象的指针传递给 producer 函数。
    std::thread tc(consumer, &q, 1);  // 创建一个消费者线程

    tp.join();  // 等待线程完成执行
    tc.join();

    printf("hello world \n");  // 主线程会等待这两个线程都完成后才继续执行。
    // 打印一下看看结果
    return 0;
}

// int main() {
//     BlockQueue<int> q(2); // 阻塞队列的初始容量为2

//     std::thread producers[3] = {
//         std::thread(producer, &q, 1),
//         std::thread(producer, &q, 2),
//         std::thread(producer, &q, 3)
//     };
    
//     std::thread consumers[3] = {
//         std::thread(consumer, &q, 1),
//         std::thread(consumer, &q, 2),
//         std::thread(consumer, &q, 3)
//     };

//     for (auto& p : producers) {
//         p.join(); // 等待生产者线程完成
//     }
    
//     for (auto& c : consumers) {
//         c.join(); // 等待消费者线程完成
//     }

//     printf("hello world\n"); // 主线程等待所有线程完成后继续执行
//     return 0;
// }

检查输出:确保

  1. 生产的顺序
    • 每个P-thread输出应该是按顺序。
    • 每次push操作后队列大小增加。
  2. 消费的顺序
    • 每个C-thread的输出应该是之前生产的数字。
    • 每次pop操作后队列大小减少。
  3. 同步问题
    • 生产者在队列满时等待。
    • 消费者在队列为空时等待。

中间也是修改了无数次,常见的问题比如元素还没被生产出来就被消费了,很奇怪,也怀疑是打印语句写的位置的问题。。具体也还没搞清楚。下面是完整的正确的程序:

#include <thread>
#include <condition_variable>
#include <mutex>
#include <queue>

#include<vector>
#include <cstdio>

template <typename T>
class BlockQueue{

    // 数据类型使用模板指定,与消费者取出的类型一样
    std::queue<T> vec;

    std::condition_variable m_not_Empty;  // 消费者线程条件变量
    std::condition_variable m_not_Full;  // 生产者线程条件变量
    std::mutex m_mtx;  // 互斥锁
    int size_maxnum;  //缓冲区大小,缓冲区中可以存储的最大元素数,这里是vec的最大容量

public:
    BlockQueue(int capacity = 10): size_maxnum(capacity) {}

    T pop(int id) {  // 消费者,意为从vector中pop数据
        std::unique_lock<std::mutex> lock(m_mtx);

        while(vec.empty()) {  // 当容量为空时等待
            m_not_Empty.wait(lock);
        }
        // m_not_Empty.wait(lock, [this]{ return !vec.empty(); });  // lambda表达式等待 

        T val = vec.front();
        vec.pop();
        printf("Queue size after pop: %lu, C-thread[%d]==>Consumed: %d\n", vec.size(), id, val);
        m_not_Full.notify_all();  // 解锁,唤醒生产者线程
        return val;
    }

    void push(const T& val, int id) {  // 生产者,意为向vector中push数据
        std::unique_lock<std::mutex> lock(m_mtx);  // 创建了一个智能锁对象 lock,将互斥量 mtx 锁住。
        
        while(vec.size() >= size_maxnum) {  // 当队列已满时等待
            m_not_Full.wait(lock);
        }
        // m_not_Full.wait(lock, [this]{ return vec.size() < size_maxnum; });

        vec.push(val);
        printf("Queue size after push: %lu, P-thread[%d]==>Produced: %d\n", vec.size(), id, val);
        m_not_Empty.notify_all();  // 解锁,唤醒消费者线程
    }
    
};

void producer(BlockQueue<int>* q, int id) {
    for (int i = 0; i < 5; ++i) {
        q->push(i + id * 100, id);  // 每个线程生产的数据都不同,便于调试
    }
}

void consumer(BlockQueue<int>* q, int id) {
    for (int i = 0; i < 5; ++i) {
        int item = q->pop(id);
    }
}

int main() {

    BlockQueue<int> q(2);  // 阻塞队列的初始容量为2

    std::thread tp(producer, &q, 1);  // 创建一个生产者线程;队列对象的指针传递给 producer 函数。
    std::thread tc(consumer, &q, 1);  // 创建一个消费者线程

    tp.join();  // 等待线程完成执行
    tc.join();

    printf("hello world \n");  // 主线程会等待这两个线程都完成后才继续执行。
    // 打印一下看看结果
    return 0;
}

// int main() {
//     BlockQueue<int> q(2); // 阻塞队列的初始容量为2

//     std::thread producers[3] = {
//         std::thread(producer, &q, 1),
//         std::thread(producer, &q, 2),
//         std::thread(producer, &q, 3)
//     };
    
//     std::thread consumers[3] = {
//         std::thread(consumer, &q, 1),
//         std::thread(consumer, &q, 2),
//         std::thread(consumer, &q, 3)
//     };

//     for (auto& p : producers) {
//         p.join(); // 等待生产者线程完成
//     }
    
//     for (auto& c : consumers) {
//         c.join(); // 等待消费者线程完成
//     }

//     printf("hello world\n"); // 主线程等待所有线程完成后继续执行
//     return 0;
// }

打印结果:
Queue size after push: 1, P-thread[1]==>Produced: 100
Queue size after push: 2, P-thread[1]==>Produced: 101
Queue size after pop: 1, C-thread[1]==>Consumed: 100
Queue size after pop: 0, C-thread[1]==>Consumed: 101
Queue size after push: 1, P-thread[1]==>Produced: 102
Queue size after push: 2, P-thread[1]==>Produced: 103
Queue size after pop: 1, C-thread[1]==>Consumed: 102
Queue size after pop: 0, C-thread[1]==>Consumed: 103
Queue size after push: 1, P-thread[1]==>Produced: 104
Queue size after pop: 0, C-thread[1]==>Consumed: 104
hello world

从最新的输出结果可以看到,生产和消费操作被正确同步,队列的大小正确更新,所有生产的项目都被唯一标识并且有序消费。这个输出结果表明生产者 - 消费者问题被正确解决。正确输出的特征包括:

  1. 生产和消费顺序:每个生产者生产的项目在队列中有序地被消费者消费。
  2. 队列大小的正确更新:每次生产和消费操作后,队列的大小正确更新。
  3. 唯一标识的生产项目:每个生产项目都有唯一的标识,确保没有重复生产或消费。

参考资料:

生产者消费者模式 (Producer-Consumer Pattern) | 并发型模式 |《Go 语言设计模式 1.0.0》| Go 技术论坛 (learnku.com)

如何实现一个生产者消费者模型(面试C++)_c++实现生产者消费者模型-CSDN博客

生产者-消费者模型:理论讲解及实现(C++) - HOracle - 博客园 (cnblogs.com)

考研操作系统精讲(408)-1.6进程的同步与互斥(生产者消费者问题)_哔哩哔哩_bilibili

C++ 实现多线程生产者消费者模式-腾讯云开发者社区-腾讯云 (tencent.com)

阻塞队列(超详细易懂)-CSDN博客

多线程编程3:C++11 互斥锁和条件变量_如何获取互斥体变量-CSDN博客

推荐阅读:
C++11 多线程操作 (线程控制、互斥锁、条件变量、原子操作、自旋锁)_c++ 带锁 join-CSDN博客

也是查了相当多的资料。。。

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值