生產者-消費者問題詳解

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語言的不熟悉導致的問題,看着我宿舍大神寫的代碼,真漂亮,由衷的羨慕。熟知並非真知,還得多加思考纔是。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章