生产者-消费者问题详解

1. 前言

  生产者-消费者问题是经典的线程同步问题(我会用java和c分别实现),主要牵扯到三个点:
 一:能否互斥访问共享资源(不能同时访问共享数据);
 二:当公共容器满时,生产者能否继续生产(生产者应阻塞并唤醒消费者消费);
 三:当公共容器为空时,消费者能否继续消费(消费者应阻塞并唤醒生产者生产)。

2. JAVA实现

step0:在java中我们创建线程是通过继承Thread类或者继承Runnable接口并实现他的run方法来实现的,这里我们采用后者
step1:定义一个放馒头的大筐(一个公共的容器类),这个筐具有push方法和pop方法,分别对应往筐中放馒头和从筐中取出馒头。由于在同一个时间段内只能有一个线程访问此方法,so,我们给这两个方法加锁。代码如下:

class SyncStack{//定义放馒头的筐,是栈,先进后出
    int index = 0;//定义筐里面馒头的编号
    WoTou[] arrWT = new WoTou[6];//定义一个引用类型的数组

    public synchronized void push(WoTou wt){//定义往筐里放馒头的方法,由于需要保证在一段特定时间里只能有一个线程访问此方法,所以用synchronized关键字
        while(index == arrWT.length){
            try{
                this.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        this.notify();
        arrWT[index] = wt;
        index ++;
    }

    public synchronized WoTou pop(){//定义从筐里往外拿馒头的方法,同理在一段时间只能有一个线程访问此方法,所以用synchronized关键字
        while(index == 0){
            try{
                this.wait();//当前的正在我这个对象访问的这个线程wait
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        this.notify();//唤醒一个等待的线程,叫醒一个正在wait在我这个对象上的线程
        index --;
        return arrWT[index];
    }
}

step2:分别定义生产者和消费者的类,他们均是不同的线程。给出代码:

class Producer implements Runnable{//定义生产者这个类,是一个线程
    SyncStack ss = null;//声明了筐子的引用变量ss,表示做馒头的人往那个筐里放馒头
    Producer(SyncStack ss){
        this.ss = ss;
    }

    public void run(){
        for(int i=0; i<20; i++){
            WoTou wt = new WoTou(i);//new出一个馒头,该馒头的编号为i
            ss.push(wt);//把第i个馒头放到筐中
            System.out.println(i);
            System.out.println("生产了:" + wt);
            try{
                Thread.sleep((int)(Math.random() * 200));//每生产一个睡眠1s
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

class Consumer implements Runnable{//定义消费者这个类,也是一个线程
    SyncStack ss = null;//声明了筐子的引用变量ss,表示吃馒头的人往那个筐里拿馒头
    Consumer(SyncStack ss){
        this.ss = ss;
    }

    public void run(){
        for(int i=0; i<20; i++){
            WoTou wt = ss.pop();//取出一个馒头
            System.out.println("消费了:" + wt);//开始吃馒头

            try{
                Thread.sleep((int)(Math.random() * 1000));
                //每消费一个睡眠1s
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

step3:我们事先定义了一个馒头类,现在给出测试类测试:

/*
wait和sleep的区别
    -1:sleep不需要唤醒,线程不会释放对象锁,属于Thread类
    -2:wait需要notify()方法唤醒,线程会放弃对象锁,属于Object类
*/
public class ProducerConsumer{
    public static void main(String[] args){
        SyncStack ss = new SyncStack();
        Producer p = new Producer(ss);
        Consumer c = new Consumer(ss);

        new Thread(p).start();
        new Thread(c).start();
    }
}

class WoTou{//定义馒头这个类
    int id;//定义馒头的编号
    WoTou(int id){
        this.id = id;
    }
    public String toString(){//重写toString方法
        return "WoTou:" + id;
    }
}

step4:看下测试结果,发现符合我们事先说的那三点:

3. C实现

step0:c语言在Windows下实现线程的需要导入#include<process.h>头文件,用_beginthread();来开始一个线程,用_endthread();来结束一个线程。具体操作方法,自行百度。
step1:C语言中缓冲区对应公共容器,我们通过定义互斥信号量mutex来实现线程对缓冲池的互斥访问。直接看下代码操作:

#include<stdio.h>
#include<process.h>
#define N 10

//代表执行生产和消费的变量
int in=0, out=0;

//线程结束的标志
int flg_pro=0, flg_con=0;

//mutex:互斥信号量,实现线程对缓冲池的互斥访问;
//empty和full:资源信号量,分别表示缓冲池中空缓冲池和满缓冲池的数量(注意初值,从生产者的角度)
int mutex=1, empty=N, full=0;

//打印测试
void print(char c){
    printf("%c    一共生产了%d个窝头,消费了%d个窝头,现在有%d个窝头\n", c, in, out, full);
}

//请求某个资源
void wait(int *x){
    while((*x)<=0);
    (*x)--;
}

//释放某个资源
void signal(int *x){
    (*x)++;
} 

//生产者
void produce(void *a){
    while(1){
//      printf("开始阻塞生产者\n");
        wait(&empty);   //申请一个缓冲区,看有无其他线程访问
        wait(&mutex);
//      printf("结束阻塞生产者\n");

        in++;

        signal(&mutex);
        signal(&full);  //full加一,唤醒消费者,告诉消费者可以消费 

//      printf("生产者执行。。。\n");
        print(‘p‘);

        Sleep(200);
        if(flg_pro == 1){
            _endthread();
        }
    }
} 

//消费者
void consumer(void * a){
    while(1){
//      printf("开始阻塞消费者\n");
        wait(&full);
        wait(&mutex);
//      printf("结束阻塞消费者\n");

        out++;

        signal(&mutex);
        signal(&empty);
//      printf("消费者执行。。。\n");
        print(‘c‘);

        Sleep(200);
        if(flg_con == 1){
            _endthread();
        }
    }
} 

//主函数
int main(){
    _beginthread(consumer,0,NULL);
    _beginthread(produce,0,NULL);
    //总的执行时间为1分钟
    Sleep(10000);
    flg_pro=flg_con=1;
    system("pause");
    return 0;
}

step2:注意事项:
  1)用来实现互斥的wait(&mutex);signal(&mutex);必须成对出现在每一个线程中,对于资源信号量的waitsignal操作,分别成对出现在不同的线程中
  2)先执行对资源信号量的wait操作,在执行对互斥信号量的wait操作,不能颠倒否则导致死锁。
step3:测试结果,符合预期:

4. 总结

 现在缺乏的是一种把生活中具体的问题抽象成代码的能力,可能也是对c语言的不熟悉导致的问题,看着我宿舍大神写的代码,真漂亮,由衷的羡慕。熟知并非真知,还得多加思考才是。

原文地址:http://blog.51cto.com/13416247/2173951

时间: 2024-11-09 05:52:18

生产者-消费者问题详解的相关文章

综合运用: C++11 多线程下生产者消费者模型详解(转)

生产者消费者问题是多线程并发中一个非常经典的问题,相信学过操作系统课程的同学都清楚这个问题的根源.本文将就四种情况分析并介绍生产者和消费者问题,它们分别是:单生产者-单消费者模型,单生产者-多消费者模型,多生产者-单消费者模型,多生产者-多消费者模型,我会给出四种情况下的 C++11 并发解决方案,如果文中出现了错误或者你对代码有异议,欢迎交流 ;-). 单生产者-单消费者模型 顾名思义,单生产者-单消费者模型中只有一个生产者和一个消费者,生产者不停地往产品库中放入产品,消费者则从产品库中取走产

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多线程第二十五章:生产者与消费者线程详解 下一章 "全栈2019"Java多线程第二十六章:同步方法生产者与消费者线程 学习小组 加入同步学习小组,共同交流与进步. 方式一:关注头条号Gorhaf,私信"Java学习小组". 方式二:关注公众号Gorhaf,回复"

java多线程同步以及线程间通信详解&amp;消费者生产者模式&amp;死锁&amp;Thread.join()(多线程编程之二)

本篇我们将讨论以下知识点: 1.线程同步问题的产生 什么是线程同步问题,我们先来看一段卖票系统的代码,然后再分析这个问题: [java] view plain copy print? package com.zejian.test; /** * @author zejian * @time 2016年3月12日 下午2:55:42 * @decrition 模拟卖票线程 */ public class Ticket implements Runnable { //当前拥有的票数 private 

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

什么是生产者消费者模式 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题.生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. 什么是生产者消费者模式 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题.生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待

Java模拟生产者消费者问题

一.Syncronized方法详解 解决生产者消费这问题前,先来了解一下Java中的syncronized关键字. synchronized关键字用于保护共享数据.请大家注意"共享数据",你一定要分清哪些数据是共享数据,如下面程序中synchronized关键字保护的不是共享数据(其实在这个程序中synchronized关键字没有起到任何作用,此程序的运行结果是不可预先确定的).这个程序中的t1,t2是 两个对象(pp1,pp2)的线程.JAVA是面向对象的程序设计语言,不同的对象的数

java多线程详解

转自:线程间通信.等待唤醒机制.生产者消费者问题(Lock,Condition).停止线程和守护线程.线程优先级 1  线程间通信 1.1  线程间通信 其实就是多个线程在操作同一个资源,但是操作的动作不同. 比如一个线程给一个变量赋值,而另一个线程打印这个变量. 1.2  等待唤醒机制 wait():将线程等待,释放了CPU执行权,同时将线程对象存储到线程池中. notify():唤醒线程池中一个等待的线程,若线程池有多个等待的线程,则任意唤醒一个. notifyAll():唤醒线程池中,所有

200道历年逻辑推理真题详解

200道历年逻辑推理真题详解 01.粮食可以在收割前在期货市场进行交易.如果预测谷物产量不足,谷物期货价格就会上升:如果预测谷物丰收,谷物期货价格就会下降.今天早上,气象学家们预测从明天开始谷物产区里会有非常需要的降雨.因为充分的潮湿对目前谷物的存活非常重要,所以今天的谷物期货价格会大幅下降. 下面哪个,如果正确,最严重地削弱了以上的观点? A.在关键的授粉阶段没有接受足够潮湿的谷物不会取得丰收. B.本季度谷物期货价格的波动比上季度更加剧烈. C.气象学家们预测的明天的降雨估计很可能会延伸到谷

LINux网络的NAPI机制详解一

在查看NAPI机制的时候发现一篇介绍NAPI引入初衷的文章写的很好,通俗易懂,就想要分享下,重要的是博主还做了可以在他基础上任意修改,而并不用注明出处的声明,着实令我敬佩,不过还是附上原文链接! http://blog.csdn.net/dog250/article/details/5302853 处理外部事件是cpu必须要做的事,因为cpu和外设的不平等性导致外设的事件被cpu 当作是外部事件,其实它们是平等的,只不过冯氏机器不这么认为罢了,既然要处理外部事件,那么就需要一定的方法,方法不止一

Kafka详解之二、如何配置Kafka集群

Kafka集群配置比较简单,为了更好的让大家理解,在这里要分别介绍下面三种配置 单节点:一个broker的集群 单节点:多个broker的集群 多节点:多broker集群 一.单节点单broker实例的配置 1.首先启动zookeeper服务 Kafka本身提供了启动zookeeper的脚本(在kafka/bin/目录下)和zookeeper配置文件(在kafka/config/目录下),首先进入Kafka的主目录(可通过 whereis kafka命令查找到): [[email protect