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;
}

运行结果:
image
分析:
1.发现会先生产10个数据->消费10个->生产10个->消费十个,为什么不是生产1个就消费1个
答:每个生产者线程在循环内连续持有锁10次(for循环+unique_lock,其中会循环持有锁,解锁10次),cv.notify_one()在持有锁时调用,可能唤醒的消费者无法立即获取锁(因生产者仍持有锁)
消费者被唤醒后需要重新竞争锁,而生产者释放锁后可能立即重新获取(循环内)。
解决办法:

  1. 设置一个容量上限(典型的就是 1),实现阻塞型缓冲区逻辑
  2. 生产者逻辑改成:如果队列满了,就等待
  3. 消费者逻辑保持不变或对称地等待 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;
}

运行效果:
image

生产者和消费者共享一块缓冲区,这里最大容量为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;
}

打印结果:
image

posted @ 2025-07-23 14:49  seekwhale13  阅读(32)  评论(0)    收藏  举报