生产者消费者模式 (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。
生产者-消费者操作示例:
- 生产者操作:
- 生产者在向缓冲区放入一个元素之前,会等待空位信号量(
empty
)大于0。 - 向缓冲区放入一个元素后,空位信号量减1,满位信号量加1。
- 生产者在向缓冲区放入一个元素之前,会等待空位信号量(
- 消费者操作:
- 消费者在从缓冲区取出一个元素之前,会等待满位信号量(
full
)大于0。 - 从缓冲区取出一个元素后,满位信号量减1,空位信号量加1。
- 消费者在从缓冲区取出一个元素之前,会等待满位信号量(
信号量的变化反映了缓冲区的使用情况,但信号量本身并不是缓冲区的大小。信号量用于协调生产者和消费者对缓冲区的访问,确保线程同步和数据完整性。
条件变量
条件变量(Condition Variable)用于让线程等待某个条件成立,并在该条件可能改变时被其他线程通知。条件变量本身没有特殊的数值属性,使用等待和通知机制来协调多个线程的运行,使线程能够在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用,可以有效地解决复杂的线程同步问题,确保线程安全地访问共享资源。
- 条件变量适用于需要在某个条件满足时唤醒一个或多个线程的情况。常用于复杂的同步场景,例如生产者-消费者模型中,当缓冲区为空或满时进行等待和通知。
- 相关操作:
wait
(有时还包括timed_wait
):线程等待条件变量,并自动释放关联的互斥锁。当被唤醒时,线程重新获取互斥锁。signal
(或notify_one
):唤醒等待在条件变量上的一个线程。broadcast
(或notify_all
):唤醒等待在条件变量上的所有线程。
生产者-消费者操作示例:
-
生产者线程:
-
生产一个数据项。
-
获取互斥锁。
-
如果缓冲区满,调用
pthread_cond_wait(&cond_empty, &mutex)
,等待空位出现,并释放互斥锁。被唤醒后,重新获取互斥锁。 -
将数据放入缓冲区。
-
通过
pthread_cond_signal(&cond_full)
通知等待在cond_full
条件变量上的消费者。 -
释放互斥锁。
-
-
消费者线程:
- 获取互斥锁。
- 如果缓冲区为空,调用
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_Empty
和 m_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;
// }
检查输出:确保
- 生产的顺序
- 每个
P-thread
输出应该是按顺序。 - 每次
push
操作后队列大小增加。
- 每个
- 消费的顺序
- 每个
C-thread
的输出应该是之前生产的数字。 - 每次
pop
操作后队列大小减少。
- 每个
- 同步问题
- 生产者在队列满时等待。
- 消费者在队列为空时等待。
中间也是修改了无数次,常见的问题比如元素还没被生产出来就被消费了,很奇怪,也怀疑是打印语句写的位置的问题。。具体也还没搞清楚。下面是完整的正确的程序:
#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
从最新的输出结果可以看到,生产和消费操作被正确同步,队列的大小正确更新,所有生产的项目都被唯一标识并且有序消费。这个输出结果表明生产者 - 消费者问题被正确解决。正确输出的特征包括:
- 生产和消费顺序:每个生产者生产的项目在队列中有序地被消费者消费。
- 队列大小的正确更新:每次生产和消费操作后,队列的大小正确更新。
- 唯一标识的生产项目:每个生产项目都有唯一的标识,确保没有重复生产或消费。
参考资料:
生产者消费者模式 (Producer-Consumer Pattern) | 并发型模式 |《Go 语言设计模式 1.0.0》| Go 技术论坛 (learnku.com)
如何实现一个生产者消费者模型(面试C++)_c++实现生产者消费者模型-CSDN博客
生产者-消费者模型:理论讲解及实现(C++) - HOracle - 博客园 (cnblogs.com)
考研操作系统精讲(408)-1.6进程的同步与互斥(生产者消费者问题)_哔哩哔哩_bilibili
C++ 实现多线程生产者消费者模式-腾讯云开发者社区-腾讯云 (tencent.com)
多线程编程3:C++11 互斥锁和条件变量_如何获取互斥体变量-CSDN博客
推荐阅读:
C++11 多线程操作 (线程控制、互斥锁、条件变量、原子操作、自旋锁)_c++ 带锁 join-CSDN博客
也是查了相当多的资料。。。