提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
文章目录
前言
一、什么是生产者-消费者问题?
二、代码的实现
1.最初始的代码
(1).一些废话:
2.第二版
(1)一些废话:
(2)循环队列:
(3)多线程编程
(4)lambda表达式与condition_variable:
(5)类中的index、in与out
3.第三版
(1)一些废话
(2)改动的代码
总结
生产者-消费者问题属于操作系统中经典的进程同步之一,而以下我将会用C++的多线程来实现生产者-消费者模型。本文提到的“书”均为《计算机操作系统》(第四版)“西安电子科技大学出版社”。
提示:以下是本篇文章正文内容,下面案例可供参考
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。——摘自《百度百科》:生产者消费者问题_百度百科
//生产者-消费者问题1
#include
#include
#include
#include
using namespace std;
//预处理
#define MAX 10//缓冲池的最大数量
//全局变量的定义
mutex m;//建立一个互斥信号量m
string buffer[MAX];//建立一个最大数量为MAX的缓冲池,此缓冲池采用循环队列的形式
int pro_num = 0;//用pro_num来记录生产者在缓冲池的当前位置
int con_num = 0;//用con_num来记录消费者在缓冲池的当前位置
int pro_index = 0;//用index来记录现在是第几个被生产者生产的产品
//生产者类
class producer{
private:
string in;//生产的产品
int index;//用来标记现在是第几个生产者
public:
//带默认形参的构造函数
producer( string in = "", int index = 0 ) : in( in ), index( index ){
}
//调用系统默认的析构函数
~producer( ) = default;
//生产产品,用time来决定循环几次
void produce( );
};
class consumer{
private:
string out;//消费的产品
int index;//用来标记现在是第几个消费者
public:
//带默认形参的构造函数
consumer( string out = "", int index = 0 ) : out( out ), index( index ){
}
//调用系统默认的析构函数
~consumer( ) = default;
//消费产品
void consume( );
};
//生产产品函数的实现
void producer::produce( ){
if( ( pro_num + 1 ) % MAX != con_num ){//判断缓冲池未满
in = "生产者“" + to_string( this->index ) + "”生产的产品" + to_string( pro_index++ );//生产的产品
m.lock( );//锁住互斥变量m,避免消费者动用缓冲池buffer
buffer[pro_num] = in;
pro_num = ( pro_num +1 ) % MAX;
cout << in << endl;
m.unlock( );//解锁互斥变量m,使消费者可以动用缓冲池buffer
}
else{//判断缓冲池已满
m.lock( );//锁住互斥变量m,避免消费者动用缓冲池buffer
cout << "缓冲池已满!" << endl;
m.unlock( );//解锁互斥变量m,使消费者可以动用缓冲池buffer
}
}
//消费产品函数的实现
void consumer::consume(){
if( pro_num != con_num ){//判断缓冲池不空
m.lock( );//锁住互斥变量m,避免生产者动用缓冲池buffer
out = "消费者“" + to_string( this->index ) + "”消费了" + buffer[con_num];
con_num = ( con_num + 1 ) % MAX;
cout << out << endl;
m.unlock( );//解锁互斥变量m,是生产者可以动用缓冲池buffer
}
else{//判断缓冲池为空
m.lock( );//锁住互斥变量m,避免生产者动用缓冲池buffer
cout << "缓冲池为空!" << endl;
m.unlock( );//解锁互斥变量m,是生产者可以动用缓冲池buffer
}
}
//定义了一个函数,让生产者生产50次产品
void producer_work( producer p ) {
int i = 0;
while( i < 50 ){
p.produce( );
i++;
}
}
//定义了一个函数,让消费者消费50次产品
void consumer_work( consumer c ){
int i = 0;
while( i < 50 ){
c.consume( );
i++;
}
}
int main( void ){
producer p;
consumer c;
thread pro( producer_work, p );
thread con( consumer_work, c);
pro.join( );
con.join( );
return 0;
}
我们可以看到这个并不理想。实际上,这也不符合我们书上的内容。书上的模型是当缓冲池满时生产者等待,缓冲池空时消费者等待。而不是像现在的代码一样缓冲池满时输出“缓冲池已满!”,缓冲池为空时输出“缓冲池为空!”。而最大的原因是最初始的代码没有使用过condition_variable库的wait()函数。因此有了第二版:
//生产者-消费者问题2
#include
#include
#include
#include
#include
using namespace std;
//预处理
#define MAX 10//缓冲池的最大数量
//全局变量的定义
mutex m;//建立一个互斥信号量m
condition_variable full_con;//如果某时刻缓冲池为满,那么就让full_con等待
condition_variable empty_con;//如果某时刻缓冲池为空,那么就让empty等待
string buffer[MAX];//建立一个最大数量为MAX的缓冲池,此缓冲池采用循环队列的形式
int pro_num = 0;//用pro_num来记录生产者在缓冲池的当前位置
int con_num = 0;//用con_num来记录消费者在缓冲池的当前位置
int pro_index = 0;//用index来记录现在是第几个被生产者生产的产品
//生产者类
class producer{
private:
string in;//生产的产品
int index;//用来标记现在是第几个生产者
public:
//带默认形参的构造函数
producer( string in = "", int index = 0 ) : in( in ), index( index ){
}
//调用系统默认的析构函数
~producer( ) = default;
//生产产品,用time来决定循环几次
void produce( );
};
class consumer{
private:
string out;//消费的产品
int index;//用来标记现在是第几个消费者
public:
//带默认形参的构造函数
consumer( string out = "", int index = 0 ) : out( out ), index( index ){
}
//调用系统默认的析构函数
~consumer( ) = default;
//消费产品
void consume( );
};
//生产产品函数的实现
void producer::produce( ){
in = "生产者“" + to_string( this->index ) + "”生产的产品" + to_string( pro_index++ );//生产的产品
unique_lock lock( m );//锁住互斥变量m,避免消费者动用缓冲池buffer
full_con.wait( lock, /*lambda表达式*/[=]{ return ( pro_num + 1 ) % MAX != con_num; }/*lambda表达式*/ );//缓冲池满时等待
buffer[pro_num] = in;//将生产者生产的产品放入缓冲池
pro_num = ( pro_num +1 ) % MAX;
cout << in << endl;
empty_con.notify_all();//唤起empty
}
//消费产品函数的实现
void consumer::consume(){
unique_lock lock( m );//锁住互斥变量m,避免生产者动用缓冲池buffer
empty_con.wait( lock, /*lambda表达式*/[=]{ return pro_num != con_num; }/*lambda表达式*/ );//缓冲池为空时等待
out = "消费者“" + to_string( this->index ) + "”消费了" + buffer[con_num];//消费者消费的产品
con_num = ( con_num + 1 ) % MAX;//将生产者生产的产品从缓冲池取出
cout << out << endl;
full_con.notify_all();//唤起full
}
//定义了一个函数,让生产者生产50次产品
void producer_work( producer p ) {
int i = 0;
while( i < 50 ){
p.produce( );
i++;
}
}
//定义了一个函数,让消费者消费50次产品
void consumer_work( consumer c ){
int i = 0;
while( i < 50 ){
c.consume( );
i++;
}
}
int main( void ){
producer p;
consumer c;
thread pro( producer_work, p );
thread con( consumer_work, c);
pro.join( );
con.join( );
return 0;
}
第二版就符合了我们书本上的要求,当缓冲池满时生产者等待,缓冲池空时消费者等待。以此,我来讲解一下代码中的一些可能需要讲解的地方。
第17行:string buffer[MAX],这里定义了一个大小为MAX的数组。我们后续使用这个数组的方式“循环队列”的形式。
循环队列可以粗浅的理解为数组的头与尾相连的一个队列。当指针到达数组尾想要下一步时会回到数组的头。而队列则是“先进先出”的一个存储结构。同时,循环队列的队空大部分为头指针与尾指针重合;而队满则是队尾指针加1与数组大小取模后与队头指针重合,而若是这样的结构,那么会有使得我们实际能使用的大小减一。队列详细可以去CSDN浏览其他大佬的文章,这里不做过多赘述。
多线程编程中,我们会使用到thread库来创建线程,用mutex库来实现互斥操作,用condition_variable库来实现条件等待。其中内容过多,详细代码请自行去浏览CSDN其他大佬的文章。
第61行:full_con.wait( lock, /*lambda表达式*/[=]{ return ( pro_num + 1 ) % MAX != con_num; }/*lambda表达式*/ );//缓冲池满时等待
Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
——取自《百度百科》:Lambda表达式_百度百科
我们先看看condition_variable库的wait()函数:
void wait( std::unique_lockstd::mutex& lock, Predicate pred );
第一个形参std::unique_lockstd::mutex& lock是一个“实现可移动的互斥体所有权包装器”,这里详细自己百度,主要是第二个Predicate pred。这里当我们的pred放回为false时,我们的wait( )函数才会阻塞进入等待,而true不会。而此时,我们使用了lambda表达式来书写。一个lambda表达式的格式为:[ capture ] ( params ) opt -> ret { body; };其中:
——摘自《C++11:lambda表达式详细介绍》:C++11:lambda表达式详细介绍_luoyayun361的博客-CSDN博客_lambda表达式c++11
我们这里因为没有参数列表与函数选项,所以直接省略,而为了方便在捕获列表中直接使用了[=]来捕获所有的外部变量,一步到位(其实就是懒),捕获可以大致理解为把在lambda表达式以外的变量“抓”进来,此时我们可以直接使用“抓”进来的变量。
而函数体中,我们仅有一个return语句,用来返回( pro_num + 1 ) % MAX != con_num的结果。这里要注意上述说过的话,仅在pred返回为false时才会阻塞,所以我们这里使用的是“!=”
第
第26行与第43行中有一个index,这里是我一开始想从一对一开始逐步写向多对多,但是由于临近死线(2h后),所以只能放弃,实际上这里的index没有什么现实意义,不过若是删去这里要记得把输出语句中想过的this->index改掉。
第59行的in与第72行的out中,我们能看到有个:
in = "生产者“" + to_string( this->index ) + "”生产的产品" + to_string( pro_index++ );//生产的产品
的操作,这里的in中的+可以直接拼接是因为string中官方就已经实现了“+”号的重载,让其可以在字符串中直接“相加”,其实就是拼接,把“+”号右边的字符串拼接到“+”号左边字符串的末尾。而 to_string在这里是把int型的index转为一个字符串以实现拼接。
//生产者-消费者问题3
#include
#include
#include
#include
#include
using namespace std;
//预处理
#define MAX 10//缓冲池的最大数量
//全局变量的定义
mutex m;//建立一个互斥信号量m
condition_variable full_con;//如果某时刻缓冲池为满,那么就让full_con等待
condition_variable empty_con;//如果某时刻缓冲池为空,那么就让empty等待
string buffer[MAX];//建立一个最大数量为MAX的缓冲池,此缓冲池采用循环队列的形式
int pro_num = 0;//用pro_num来记录生产者在缓冲池的当前位置
int con_num = 0;//用con_num来记录消费者在缓冲池的当前位置
int pro_index = 0;//用index来记录现在是第几个被生产者生产的产品
//生产者类
class producer{
private:
string in;//生产的产品
int index;//用来标记现在是第几个生产者
public:
//带默认形参的构造函数
producer( string in = "", int index = 0 ) : in( in ), index( index ){
}
//调用系统默认的析构函数
~producer( ) = default;
//生产产品,用time来决定循环几次
void produce( );
};
class consumer{
private:
string out;//消费的产品
int index;//用来标记现在是第几个消费者
public:
//带默认形参的构造函数
consumer( string out = "", int index = 0 ) : out( out ), index( index ){
}
//调用系统默认的析构函数
~consumer( ) = default;
//消费产品
void consume( );
};
//生产产品函数的实现
void producer::produce( ){
in = "生产者“" + to_string( this->index ) + "”生产的产品" + to_string( pro_index++ );//生产的产品
unique_lock lock( m );//锁住互斥变量m,避免消费者动用缓冲池buffer
while(( pro_num + 1 ) % MAX == con_num ){//判断缓冲池已满
cout << "【!!!】缓冲池已满,生产者“" << index << "”在等待一个空位" << endl;
full_con.wait( lock );//让full进入等待状态
}
buffer[pro_num] = in;//将生产者生产的产品放入缓冲池
pro_num = ( pro_num +1 ) % MAX;
cout << in << endl;
empty_con.notify_all();//唤起empty
}
//消费产品函数的实现
void consumer::consume(){
unique_lock lock( m );//锁住互斥变量m,避免生产者动用缓冲池buffer
while( pro_num == con_num ){//判断缓冲池为空
cout << "【!!!】缓冲池为空,消费者“" << index << "”在等待一件产品" << endl;
empty_con.wait( lock );
}
out = "消费者“" + to_string( this->index ) + "”消费了" + buffer[con_num];//消费者消费的产品
con_num = ( con_num + 1 ) % MAX;//将生产者生产的产品从缓冲池取出
cout << out << endl;
full_con.notify_all();//唤起full
}
//定义了一个函数,让生产者生产50次产品
void producer_work( producer p ) {
int i = 0;
while( i < 50 ){
p.produce( );
i++;
}
}
//定义了一个函数,让消费者消费50次产品
void consumer_work( consumer c ){
int i = 0;
while( i < 50 ){
c.consume( );
i++;
}
}
int main( void ){
producer p;
consumer c;
thread pro( producer_work, p );
thread con( consumer_work, c);
pro.join( );
con.join( );
return 0;
}
其实在第二版就已经符合书上的要求了,但是我们并不知道真的在缓冲池已满或为空时我们的生产者与消费者的情况,这里我稍微改了一下代码。
我改动的是第58行与第72行出的代码,即produce( )函数与consume()函数的实现代码,让它在已满或为空时输出一些提示,当然,不能忘记了wait操作。注意,这里没有直接使用wait( )函数的第二个形参,所以这里的表达式为“( pro_num + 1 ) % MAX == con_num”与“pro_num == con_num”,毕竟这里使用的是while函数,当里面的表达式为真时才会进入循环。
还有啥总结好写的,基本上可以写的都写了。至于我为什么还要加上这个版块?那是因为模板上有我就懒得删了,反正也就让大伙时间-1s。