c++ 例题学习:生产者-消费者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> buffer;
int sum = 0;
std::mutex mtk1;
//producer
void producer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock1(mtk1);
buffer.push(i);//produce
std::cout << "produced num:" << i << std::endl;
}
}
//consurmer
void consumer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock2(mtk1);
sum = sum + buffer.front();
buffer.pop();
std::cout << "consume num:" << i << std::endl;
}
}
int main() {
std::thread producer1(producer);
std::thread producer2(producer);
std::thread consumer1(consumer);
std::thread consumer2(consumer);
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
std::cout << "the final sum is:" << sum << std::endl;
}
问题:
1.不管是生产者之间还是消费者之间,都要保证线程安全,也就是同一时间只能有一个生产者/消费者,那肯定要使用锁mutex,问题是,生产者和消费者要使用不同的锁(mtk1,mtk2)吗?还是同一把锁(mtk1)?
答:std::mutex 的作用范围是进程内所有线程 ,只要这些线程在访问共享资源时正确获取同一个互斥量 。如果 consumer 函数中访问 buffer 的代码也通过 mtk1 加锁 ,因此mtk1 能限制 consumer 线程访问 buffer。
2.能不能同时生产和消费?
答:
!!!不可以生产和消费同时进行,因为这里用的是std::queue,不是线程安全的容器,queue 的底层是 deque,你可能会遇到这样的情况:
- 生产者在 push_back() 时触发了扩容(需要移动旧数据或新建内存)
- 与此同时,消费者在 pop_front() 时试图释放旧空间
- 这两者的操作都修改了 deque 的内部结构体,如 _Map, _First, _Finish 指针等
- 线程1和线程2同时访问,导致访问非法内存或者数据损坏
发散问题,如果用的不是std::queue,是否可以生产和消费同时进行?
答:是可以的,只要你使用了支持并发访问的容器或数据结构,就能做到生产和消费线程并发执行、互不阻塞。下面我带你发散分析几种情况。
3.如何确保先生产后消费,这里可以用条件变量,if可以吗?
不能使用if,当缓冲区中没有数据时,应当进入阻塞,而不是if不满足条件就跳过
修改后代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> buffer;
std::atomic<int> sum = 0;
std::mutex mtk1;
std::condition_variable cv;
//producer
void producer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock1(mtk1);
buffer.push(i);//produce
cv.notify_one();
std::cout << "produced num:" << i << std::endl;
}
}
//consurmer
void consumer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock2(mtk1);
cv.wait(lock2, [] {return !buffer.empty(); });
int val = buffer.front();
sum += val;
buffer.pop();
std::cout << "consume num:" << val << std::endl;
}
}
int main() {
std::thread producer1(producer);
std::thread producer2(producer);
std::thread consumer1(consumer);
std::thread consumer2(consumer);
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
std::cout << "the final sum is:" << sum << std::endl;
}
运行结果:

分析:
1.发现会先生产10个数据->消费10个->生产10个->消费十个,为什么不是生产1个就消费1个
答:每个生产者线程在循环内连续持有锁10次(for循环+unique_lock,其中会循环持有锁,解锁10次),cv.notify_one()在持有锁时调用,可能唤醒的消费者无法立即获取锁(因生产者仍持有锁)
消费者被唤醒后需要重新竞争锁,而生产者释放锁后可能立即重新获取(循环内)。
解决办法:
- 设置一个容量上限(典型的就是 1),实现阻塞型缓冲区逻辑
- 生产者逻辑改成:如果队列满了,就等待
- 消费者逻辑保持不变或对称地等待 buffer 非空
最终代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> buffer;
std::atomic<int> sum = 0;
std::mutex mtk1;
std::mutex cout_mtx;
std::condition_variable cv_producer, cv_consumer;
const int Buffer_Max_Size = 2;
//producer
void producer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock1(mtk1);
cv_producer.wait(lock1, [] {return buffer.size() < Buffer_Max_Size; });
buffer.push(i);//produce
lock1.unlock();
cv_consumer.notify_one();
std::lock_guard<std::mutex> lock(cout_mtx);
std::cout << "produced num:" << i << std::endl;
}
}
//consurmer
void consumer() {
for (int i = 1; i <= 10; i++) {
std::unique_lock<std::mutex> lock2(mtk1);
cv_consumer.wait(lock2, [] {return !buffer.empty(); });
int val = buffer.front();
sum += val;
buffer.pop();
lock2.unlock();
cv_producer.notify_one();
std::lock_guard<std::mutex> lock(cout_mtx);
std::cout << "consume num:" << val << std::endl;
}
}
int main() {
std::thread producer1(producer);
std::thread producer2(producer);
std::thread consumer1(consumer);
std::thread consumer2(consumer);
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
std::cout << "the final sum is:" << sum << std::endl;
}
运行效果:

生产者和消费者共享一块缓冲区,这里最大容量为1,生产完成后,首先检查有没有剩余容量,如果有则唤醒把数据放入缓冲区(持锁),完成后释放锁并唤醒消费者,消费者先检查缓冲区有没有数据,如果有,则持锁读取数据,释放锁并唤醒生产者,然后处理数据。
注意:
使用锁和互斥量的目的是为了同步对共享资源(如缓冲区)的访问,防止多个线程同时修改数据而引发数据竞争。但真正的生产和消费过程(例如生成一个数据、处理一个任务)通常是耗时的操作。如果将这些耗时操作也包含在加锁区域内,会导致锁被长时间占用,降低并发性能。因此,应该在加锁之前完成主要的生产或消费操作,只在加锁区域内进行共享资源的快速访问(如放入或取出缓冲区),以提高整体效率。简而言之,锁只用于保护共享资源,而不应阻塞整个生产或消费过程。
模拟操作:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
// 缓冲区
std::queue<int> buffer;
const int MAX_BUFFER_SIZE = 5;
// 同步工具
std::mutex mtk1;
std::condition_variable cv;
// 模拟生产
int do_produce(int i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时
return i;
}
// 模拟消费
void do_consume(int val) {
std::this_thread::sleep_for(std::chrono::milliseconds(150)); // 模拟耗时
}
// 生产者线程
void producer() {
for (int i = 1; i <= 10; ++i) {
int num = do_produce(i); // 🔹锁外生产
std::unique_lock<std::mutex> lock(mtk1);
cv.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; });
buffer.push(num);
std::cout << "produced num:" << num << std::endl;
cv.notify_all(); // 通知消费者
}
}
// 消费者线程
void consumer() {
for (int i = 1; i <= 10; ++i) {
int num;
{
std::unique_lock<std::mutex> lock(mtk1);
cv.wait(lock, [] { return !buffer.empty(); });
num = buffer.front();
buffer.pop();
std::cout << "consume num:" << num << std::endl;
cv.notify_all(); // 通知生产者
}
do_consume(num); // 🔹锁外消费
}
}
int main() {
std::thread p1(producer);
std::thread p2(producer);
std::thread c1(consumer);
std::thread c2(consumer);
p1.join();
p2.join();
c1.join();
c2.join();
return 0;
}
打印结果:


浙公网安备 33010602011771号