操作系统笔记:生产者与消费者问题

生产者与消费者问题是进程互斥与同步中的一个经典例子,有关这个问题的描述如下:

问题描述

现存在P个生产者和C个消费者,每个生产者一次可以向缓冲池中放入一个产品,每个消费者可以向缓冲池中一次取出一个产品,设缓冲池的大小为N。请设计一个调度的策略,来保证生产者与消费者之间动作的同步与互斥。

问题剖析与建模

问题中已经给出了提示,就是利用进程的互斥和同步来解决此问题。此问题中,所谓的缓冲池就是一个临界资源,可以用一个大小为buf_size的数组buffer[buf_size]来表示,生产者和消费者的动作相当于两个进程——producer()和consumer(),两个进程对应的函数一个负责将缓冲池的一部分置1,另一个负责将缓冲池的一部分置0(不妨设0位无产品,1位有产品)。

至于producer()和consumer()进程应该在执行时应该对缓冲池的哪部分进行操作,就可以使用数据结构中的循环队列模型:先定义两个指针in和out,创建生产者和消费者两个进程前,in和out先指向数组的首地址所在的单元,每当生产者生产一个商品,in循环加1,即in=(in+1)%buf_size,out也是如此。当in=out时,表示缓冲池中没有产品,(in+1)%buf_size==out,表示缓冲池已满

最后再来分析其中的互斥与同步关系。缓冲池有着两种极端情况,即缓冲池全满或全空,全满情况下,生产者无法再生产产品,全空情况下,消费者无法再消费产品,那么这两种情况下如何来限制生产者或消费者来生产或消费产品呢,很显然就是追加两个信号量——full与empty,full为满缓冲池的个数,empty为空缓冲池的个数。生产者想生产产品,先wait一下empty,后signal一下full,表示“生产者获得了生产机会,完成了生产,缓冲池产品数加1”消费者的行为以及含义就与生产者相反

其次,再考虑producer()和consumer()两个操作是否必须是原子操作,即操作时是否可以产生外部中断。我们将多个函数按顺序并发执行,可以发现,若P == C == 1(即生产者与消费者数量均为1)时,上述两操作并发是不会产生混乱的,然而当P和C为多个且不相等时,有可能一个生产者将缓冲池一部分置1后,未来得及in循环加一,而此时另一生产者又对这部分的缓冲池置1,从而实质上未使缓冲池产品增加,这就导致了程序的混乱,这也就衍生出了另一个互斥问题:缓冲池一次只能供一个消费者或生产者消费或生产实现此互斥的方法,就是追加一个缓冲池互斥锁——mutex,mutex设置为1,表示“有一个机会供生产者或消费者对缓冲区进行操作”,某个进程申请到操作机会后,对mutex进行wait操作,之后再进行signal操作

过程源码

本人就上述问题分析之后,写了下面的C源代码,并成功通过了测试:
pro_con.c:

#include 
#include 
#include 
#include 

sem_t			sem_mutex, sem_full, sem_empty;
int				buf_size;
int				in = 0;
int 			out = 0;

int sleepTime();
void *producer(void *argu);
void *consumer(void *argu);

int main(int argc, char **argv)
{
	int 			pro = 0;
	int 			con = 0;
	int				total_pro, total_con;

again:	printf("Please input the quantity of producer:\n");
		scanf("%d", &total_pro);
		printf("Please input the quantity of consumer:\n");
		scanf("%d", &total_con);
		printf("Please input the size of buffer:\n");
		scanf("%d", &buf_size);	
	if(total_pro<=0 || total_con<=0 || buf_size<=0)
	{
		printf("\nInvalid argument: All above of the data must be a positive integer!\n");
		printf("Please input the data again\n\n");
		goto again;
	}
	pthread_t		*thread_pro = (pthread_t*)malloc(sizeof(pthread_t)*total_pro);
	pthread_t		*thread_con = (pthread_t*)malloc(sizeof(pthread_t)*total_con);
	
	sem_init(&sem_mutex, 0, 1);
	sem_init(&sem_full, 0, 0);
	sem_init(&sem_empty, 0, buf_size);
	
	while(pro!=total_pro && con!=total_con)
	{
		pthread_create(&thread_pro[pro], NULL, producer, NULL);
		pthread_create(&thread_con[con], NULL, consumer, NULL);
		pro++;
		con++;
	}
	printf("\n**********Accomplish to create all thread!***********\n\n");
	
	while(1);
	
	return 0;
}

int sleepTime()
{
	int 		sleep_time;
	while(1)
	{
		time_t		period = time(NULL);
		srand(period);
		sleep_time = rand()%10;
		if(sleep_time > 0 && sleep_time < 3)
		{	
			return sleep_time;
		}
	}
}

void *producer(void *argu)
{
	while(1){
		sem_wait(&sem_empty);
		sem_wait(&sem_mutex);
		printf("###Producer put a thing into buffer[%d] sizeof(buffer)==%d \n", in, buf_size);
		in = (in+1)%(buf_size);
		sem_post(&sem_mutex);
		sem_post(&sem_full);
		sleep(sleepTime());
	}
}

void *consumer(void *argu)
{
	while(1){
		sem_wait(&sem_full);
		sem_wait(&sem_mutex);
		printf("$$$Consumer got a thing from buffer[%d] sizeof(buffer)==%d \n", out, buf_size);
		out = (out+1)%(buf_size);
		sem_post(&sem_mutex);
		sem_post(&sem_empty);
		sleep(sleepTime());
	}
}

有关生产者与消费者问题的伪代码在各个版本的操作系统教科书上都有,在这里,本人对代码的可实用性进行了扩展,使得实现的过程更接近真实情形。

简要地说明一下上述源代码的逻辑:

  • 有关的信号量full、empty、mutex定义为全局变量,各个消费者和生产者之间共享;
  • 用户可以手动输入消费者和生产者的数量、以及缓冲池的大小;
  • 将生产者和消费者的行为从进程弱化为线程,此操作减小了系统开销,并且实现步骤简单;
  • 生产者线程和消费者线程依次按需创建,两种线程都创建完毕时,打印线程创建完毕的信息到控制台终端
  • 定义一个函数sleepTime(),用于产生一个范围在0~3之间的随机整数,将此整数作为生产者或消费者线程中的延时时间(调用sleep()函数实现延时);
  • 此程序使用的是一个死循环,需要结束程序执行时,请ctrl_
  • 本程序源码由某汤姓老师的伪代码改编而成,程序的编写涉及多线程编程。

程序运行与运行结果

上述程序可在Linux和Windows两个系统上运行。在Linux环境下,程序的编译和运行命令为:

gcc pro_con.c -lpthread -o pro_con
./pro_con

Windows下直接打开C/C++编译软件编译,便可生成.exe文件,执行此文件即可。

程序执行结果演示:

  • Windows环境:
    操作系统笔记:生产者与消费者问题_第1张图片
  • Linux环境:
    操作系统笔记:生产者与消费者问题_第2张图片
    可以发现,程序运行中能够有效地实现诸线程间的互斥与同步

你可能感兴趣的:(计算机基础理论)