【操作系统复习随手记】(二)生产者-消费者问题及其解决方案

314 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情 这也是第22篇文章

生产者-消费者问题

废话少说,先上代码

#define N 100
#define true 1
int count=0;

void producer(void){
    int item;
    while(true){
        //产生数据项
        item=produce_item();
        if(count==N){
            sleep();
        }
        insert_item();
        count+=1;
        if(count==1){
            //说明是从0到1,按理说前面consumer在休眠,应将其唤醒
            wakeup(consumer);
        }
    }
}

void consumer(void){
    int item;
    while(true){
        if(count==0){
            sleep();
        }
        item=remove_item();
        count-=1;
        if(count==N-1){
            //说明是N->N-1,前面producer被抑制,应将其唤醒
            wakeup(producer);
        }
        consume_item(item);
    }
}

问题的关键在于:有可能出现其中一个并没有sleep,但是又没有工作,而另一个后面却去唤醒它(自然是不成功的),这样一来wakeup信号量丢失了,最后,前一个就真的永远sleep了,后一个也跟着sleep了。

为什么会出现这种情况?

原因在于竞争条件的出现。而正是访问count变量的时序可能会导致冲突。

假设消费者先访问count,发现它是0,待睡未睡的时候,生产者也读取count值并且给它加了1,再读取发现它变成1了,于是按程序设定去唤醒消费者,但消费者本来就没睡,于是信号量丢失,但之后它才开始执行sleep()(因为它在 if(count==0)的代码间隔时间中,没有去读取新值,它不知道值已经更新了,于是之后就真的睡过去了。)

但按照程序设定,只有当count为1时生产者才会去唤醒消费者,也就是说消费者已经彻底错过了被唤醒的机会。之后随着count==N,生产者也睡过去了,并且不再有去唤醒它的进程。

解决方案

唤醒等待位

前面提到,问题的关键在于第一个wakeup信号丢失了,所以唤醒等待位的作用其实就是记录这个信号。有wakeup信号时该位置1,再次睡眠时该位又清零。

有多少个信号唤醒等待位就设多少位。

  • 缺点:治标不治本。对于一两个wakeup信号还好,多了位数和进程数就呈线性关系了,不实际。

信号量(semaphore)

代码表示:

//semaphore
#define N 100
typedef int semaphore;
semaphore full=0,empty=N,mutex=1;//最后一个标记互斥

void producer(void){
    int item;
    while(true){
        item=produce_item();
        down(&empty);//对应于P
        down(&mutex);
        insert_item(item);
        up(&mutex);//对应于V
        up(&full);
    }
}

void consumer(void){
    int item;
    while(true){
        down(&full);
        down(&mutex);
        item=remove_item();
        up(&mutex);
        up(&empty);
        consume_item(item);
    }
}

注意:如果两个down之间的顺序调换,可能会导致死锁。

Q:什么时候会出现?

A:缓冲区完全满时,生产者被阻塞,mutex=0,下一轮消费者执行一下down(&mutex),也被阻塞。

管程

什么是管程

一个由过程、变量及数据结构等组成的集合,组成了一个特殊的模块或软件包。 进程可以任意调用管程中的过程(maybe进程?,不懂原文是不是procedure,指的又是进程还是过程,感觉进程调用进程的说法有点别扭),但不能绕过管程直接访问其中的数据结构。

注意事项

  • 管程是一种语言概念,少数语言有,如Java;而c之类的没有(所以不能用c写管程代码,编译器不识别)
  • 任一时刻管程中只能有一个活跃进程
  • 管程中的 互斥(mutex) 由编译器负责
  • 管程中的自动互斥一定程度上保证了时序,比如不会出现wait(类似sleep)之前signal(类似wakeup)的情况 可以将条件变量与管程结合使用
  • 条件变量不能当信号量使用,因为发给它的信号可能会丢失。

Q:什么时候?

A:一个没等待进程 的条件变量不会接收信号

Java管程解决生产者——消费者问题

Java没有内嵌的条件变量,但有wait(),等价于sleep();notify(),等价于wakeup(),但不受竞争条件约束。

public class ProducerConsumer{
    static final int N=100;
    static producer p=new producer();
    static consumer c=new consumer();
    static monitor mon=new monitor();

    public static void main(String[] args) {
        p.start();
        c,start();
    }

    static class producer extends Thread{
        public void run(){
            int item;
            while(true){
                item=produce_item();
                mon.insert(item);
            }
        }

        private int produce_item(){
            /*这里写生产代码 */
        }
    }

    static class consumer extends Thread{
        public void run(){
            int item;
            while(true){
                item=mon.remove();
                consume_item(item);
            }
        }

        private int consume_item(){
            /*这里写生产代码 */
        }
    }

    static class monitor{
        private int buffer []=new int [N];
        private int count=0,lo=0,hi=0;

        public synchronized void insert(int val){
            if(count==N) go_to_sleep();
            buffer[hi]=val;
            hi=(hi+1)%N;
            count+=1;
            if(count==1) notify();
        }

        public synchronized int remove(){
            int val;
            if(count==0) go_to_sleep();
            val=buffer[lo];
            lo=(lo+1)%N;
            count-=1;
            if(count==N-1) notify();
            return val;
        }

        private void go_to_sleep(){
            try {
                wait();
            } catch (InterruptedException exc) {
                // TODO: handle exception
                
            };
        }
    }
}

消息队列

  • 缓冲(mailbox)
#define N 100
#define TRUE 1
void produce(void){
    int item;
    message m;
    while(TRUE){
        item=produce_item();
        receive(consumer,&m);
        build_message(&m,item);
        send(consumer,&m);
    }
}

void consumer(){
    int item;
    message m;
    for(int i=0;i<N;i++){
        send(producer,&m);
    }
    while(TRUE){
        receive(producer,&m);
        item=extract_item(&m);
        send(producer,&m);
        consume_item(item);
    }
}
  • 取消缓冲——会合(rendezvous)