生产者消费者C++实现

文章详细介绍了生产者消费者问题的实现,使用C++11的条件变量和互斥锁实现线程同步。同时,讨论了PV操作和信号量的概念,以及它们在进程同步和互斥中的作用。还提到了条件变量的wait、notify_one和notify_all方法的使用及区别。
摘要由CSDN通过智能技术生成

生产者消费者

问题描述

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)

生产者、消费者共享一个初始为空、大小为n的缓冲区。
  只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。
  只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。
  缓冲区是临界资源,各进程必须互斥地访问。

在刚开始,由于这个缓冲区全部是空的,所以生产者进程可以生产一些产品,把它放入到这个缓冲区当中。那一直到这个缓冲区被充满了之后,如果此时生产者进程,它还想继续生产产品,并且把它充入缓冲区的话,那这个行为很显然应该是被阻止的,因为此时缓冲区的这些数据已经被装满了,那只有这个缓冲区腾出别的空闲的空间之后,生产者进程才可以继续往里边放出去,所以在这个时候,只能切换为消费者进程来消费这些数据,也就是从缓冲区当中取走其中的一些产品或者说数据,只要缓冲区当中有一个或者大于一个的空闲的空间,那么此时就可以唤醒生产者进程,让他从阻塞态又重新回到就绪队列,当然这个唤醒并不意味着生产者进程就立即回处理机运行,它只是回到了就绪队列而已,所以接下来有可能是消费者进程继续执行那么每一次每一轮执行都会从缓冲区取走一轮,并且使用,那一直到这个缓冲区被取空了之后,如果此时消费者进程还继续尝试从缓冲区当中取走产品的话,那由于此时已经为空了,那么这个时候这个取产品的行为,应该是被阻止了,所以消费者进程应该被阻塞,而只有生产的进程再往里边放数据的时候,消费者进程才可以在重新被唤醒,又重新回到了就是就绪队列,这个缓冲区它属于一种临界资源,各个进程是必须互斥的访问的,假如说我们的这个系统当中有两个生产者竞争,那此时这两个生产者进程在检查了之后,发现缓冲区的这些位置,每个地方都是空的,那这个生产者进程他可能就往这个位置,充入了一个它自己的产品,也就是数据,而另一个生产者进程在之前的那个检查当中也发现这个缓冲区所有的地方都是空的,那么在并发的环境下,就有可能导致这个生产者进程,它同时也在检查了之后,也往这个地方充入了一个数据,所以这就导致了前者的数据背后的数据给覆盖的情况,因此我们是必须保障缓冲区是被互斥地访问的。

#include <iostream>
#include <deque>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <vector>
#include <windows.h>
using namespace std;

const unsigned int PRODUCT_COUNT = 5;
const unsigned int CONSUMER_COUNT = 20;

const unsigned int MAX_CACHE = 10;

deque<int> que;
condition_variable cond;
mutex mtx;

void producer()
{	
	while (true) {
		Sleep(10);	//#include <windows.h>,单位是Milliseconds
		static int data = 0; //用int data=0;的话每个生产者线程都会从1开始计数
		unique_lock<mutex> lck(mtx);
		while (que.size() > MAX_CACHE) {
			cond.wait(lck); //wait会释放锁
		}

		que.push_front(++data);
		cout << this_thread::get_id() << " producer push " << data << endl;
		//lck.unlock();	//wait会释放锁,不用unlock了
		Sleep(500); //Milliseconds
		cond.notify_all();
	}
}

void consumer()
{
	while (true) {
		unique_lock<mutex> lck(mtx);
		while (que.empty()) {	//if (que.empty())会挂掉,因为判断一次为空,就往下执行que.back()了,越界导致程序奔溃
			cond.wait(lck); //wait会释放锁
		}
		int data = que.back();
		cout << this_thread::get_id() << " consumer pop " << data << endl;
		this_thread::sleep_for(chrono::milliseconds(500));
		que.pop_back();
		
		//lck.unlock();	//wait会释放锁,不用unlock了
		cond.notify_all();
	}
}

int main()
{
	vector<thread> vct;
	for (int i = 0; i<CONSUMER_COUNT; ++i) {
		vct.push_back(thread(consumer));
	}
	for (int i = CONSUMER_COUNT; i < PRODUCT_COUNT + CONSUMER_COUNT; ++i) {
		vct.push_back(thread(producer));
	}
	
	for (int i = 0; i < PRODUCT_COUNT + CONSUMER_COUNT; ++i) {
		if (vct[i].joinable()) {
			vct[i].join();
		}
	}
	
	return 0;
}

生产者消费者示例

PV操作

1. 什么是PV操作

PV操作是由P操作原语和V操作原语组成(原语是不可能中断的过程),操作对象是信号量。具体的:
  P(S):① 将信号量S的值减1,即S=S-1;② 如果S>=0,则该进程继续执行;否则进程进入等待队列,置为等待状态。
  V(S):① 将信号量S的值加1,即S=S+1;② 如果S>0,则该进程继续执行;否则释放等待队列中第一个等待信号量的进程。(因为将信号量加1后仍然不大于0,则表示等待队列中有阻塞的进程。)

2. PV操作的意义

使用PV操作和信号量可以实现进程间的同步和互斥。

3. 什么是信号量

信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号的下一个进程。信号量的值与相应资源的使用情况有关。当信号量的值大于0时,表示当前可用资源的数量;当信号量的值小于0时,其绝对值表示当前阻塞等待使用该资源的进程个数。(信号量值只能用PV操作来改变。)
  一般的,当信号量S>=0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须阻塞等待别的进程释放该类资源才能继续运行。
  而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S<=0,表示此刻有进程正在阻塞等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

条件变量

在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

condition_variable是一个类,常和mutex搭配使用。
condition_variable类是一个同步原语,可用于阻塞一个线程或同时阻止多个线程,直到另一个线程修改共享变量并通知condition_variable。
防止多线程场景下,共享变量混乱。
理解条件变量要先理解三个概念:

  1. 锁 (锁住共享变量,线程独占)
  2. wait 等待 (等待通知条件变量,变化的共享变量是否满足条件)
  3. notify 通知 (通知等待的条件变量,共享变量发送变化)

主要成员函数如下:
条件变量condition_variable类的主要成员变量

具体函数:

1、wait函数:

(1)wait(unique_lock &lck)

当前线程的执行会被阻塞,直到收到 notify 为止。

(2)wait(unique_lock &lck,Predicate pred)

当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞。

wait()可依次拆分为三个操作:释放互斥锁、等待在条件变量上、再次获取互斥锁

2、notify_one:

notify_one():没有参数、没有返回值。

解除阻塞当前正在等待此条件的线程之一。如果没有线程在等待,则还函数不执行任何操作。如果超过一个,不会指定具体哪一线程。

#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <vector>
using namespace std;

mutex mtx;
condition_variable condition_producer;
condition_variable condition_consumer;
int global = 0;

void producer(int id)
{
	unique_lock<mutex> lck(mtx);
	while (global != 0) {
		condition_producer.wait(lck);	//阻塞当前producer线程,直到收到notify,wait会自动释放锁
	}
	global = id;
	condition_consumer.notify_one();	//只唤醒一个consumer线程
}

void consumer()
{
	unique_lock<mutex> lck(mtx);
	while (global == 0) {
		condition_consumer.wait(lck);	//阻塞当前consumer线程,直到收到notify,wait会自动释放锁
	}
	cout << global << endl;
	global = 0;
	condition_producer.notify_one();	//只唤醒一个producer线程
}


const unsigned int PRODUCT_COUNT = 10;
const unsigned int CONSUMER_COUNT = 10;

int main()
{
	vector<thread> vct;

	//需要先执行消费者线程
	//先创建生产者线程的话,消费者线程还没创建,producer()函数就执行完了,consumer()中的条件变量来不及触发,不会打印值
	for (int i = 0; i < CONSUMER_COUNT; ++i) {
		vct.push_back(thread(consumer));
	}
	for (int i = CONSUMER_COUNT; i < PRODUCT_COUNT + CONSUMER_COUNT; ++i) {
		vct.push_back(thread(producer, i));
	}

	for (int i = 0; i < PRODUCT_COUNT + CONSUMER_COUNT; ++i) {
		if (vct[i].joinable()) {
			vct[i].join();
		}
	}

	return 0;
}

PV操作示例

3、notify_all:

多个线程在调用条件变量的wait方法时会阻塞住

notify_one:此时调用notify_one会随机唤醒一个阻塞的线程,而其余的线程将仍然处于阻塞状态,等待下一次唤醒。

notify_all:调用notify_all则会唤醒所有线程,线程会争抢锁,当然只有一个线程会获得到锁,而其余未获得锁的线程也将不再阻塞,而是进入到类似轮询的状态,等待锁资源释放后再去争抢。

假如同时有10个线程阻塞在wait方法上,则需要调用10次notify_one,而仅仅只需要调用1次notify_all

notify_one()与notify_all()常用来唤醒阻塞的线程,线程被唤醒后立即尝试获得锁。

notify_one()因为只唤醒一个线程,不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,等待再次调用notify_one()或者notify_all()。

notify_all()会唤醒所有阻塞的线程,存在锁争用,只有一个线程能够获得锁。那其余未获取锁的线程接着会怎么样?会阻塞?还是继续尝试获得锁?答案是会阻塞,等待操作系统在互斥锁的状态发生改变时唤醒线程。当持有锁的线程释放锁时,操作系统会唤醒这些阻塞的线程,而这些线程会继续尝试获得锁。

示例见:https://blog.csdn.net/xp178171640/article/details/106016141

参考:

deque:

emplace和insert的区别:
emplace和insert插入元素最大的区别是emplace不会产生不必要的变量,使用insert插入元素时,需要申请内存空间创建临时对象,而申请内存空间就需要消耗一定时间;而使用emplace插入元素时,直接在原来容器的内存空间上 ,调用构造函数,不需要额外申请内存空间,就节省了很多时间,效率较高。
emplace_back()——在deque尾部插入元素
在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。
emplace_front()——在deque尾部插入元素
在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。

https://blog.csdn.net/weixin_46522531/article/details/127702340

生产者消费者问题:

https://blog.csdn.net/weixin_45990326/article/details/119909449
https://blog.csdn.net/dalao_whs/article/details/109009484

PV操作:

https://blog.csdn.net/m0_51439095/article/details/124824317?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168984089116800186582565%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=168984089116800186582565&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-124824317-null-null.142v90insert_down1,239v2insert_chatgpt&utm_term=PV%E6%93%8D%E4%BD%9C&spm=1018.2226.3001.4187

条件变量:

https://blog.csdn.net/whl0071/article/details/126390567
https://blog.csdn.net/kingforyang/article/details/121665393

notify_one()与notify_all()的区别:

https://blog.csdn.net/xp178171640/article/details/106016141
https://blog.csdn.net/Think88666/article/details/122478298

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值