操作系统笔记:(九)信号量与经典同步问题

这节讲解操作系统用信号量机制解决同步问题,先讲解他的基本实现,然后在讲解用信号量机制解决经典的同步问题:

  • 生产者消费者问题
  • 读者写者问题
  • 哲学家就餐问题

主要讲的进程同步方法如下,这一节讲信号量,下一节讲解管程

这里写图片描述

remark 这一节进程和线程的概念通常是互通的,不加详细区分,仅作为CPU的调度单位

信号量是什么

信号量是os提供的管理同步问题的一种手段,具体来说,他有一个整数变量记录当前可供使用的标记信号,同时提供两种原子操作:

  • P(): (荷兰语 probern, 测试)
  • V(): (荷兰语, verhogen, 增加)

伪代码实现

typedef struct{
int value;
PCB *list;//等待队列
}Semaphore;

void P(Semaphore * s){
    if(--S->value <0){
        add current thread to s->list;
        block();//阻塞当前线程
    }
}
void V(Semaphore * s){
    if(++S->value <=0){
        remove a process P from s->list;
        wakeup(P);//唤醒一个线程
    }
}

实现

为了使P,V操作实现原子操作,通常在单处理器中我们可以用关中断。注意,因为P,V操作的临界区很短,可以看到,也就10条指令左右就可以实现,因此直接关中断是很好的解决方法,而之前讲的在用户程序中关中断同步不好,其中一个很大的原因就是用户程序不好预测,临界区很长。但是多处理器关中断就不好了。我们这里不妨假设这是Cpu硬件和OS提供的一种原子操作。

临界区互斥与过程同步

我们可以用信号量来实现临界区互斥访问和多某些代码块的同步访问

临界区互斥访问

Semaphore mutex = new Semaphore(1);//设置为1 就相当于实现互斥锁
while(1){
    mutex.P(); //进入临界区,获取互斥信号量
    // 临界区访问
    mutex.V();// 释放互斥信号量
}

同步代码先后顺序

这里写图片描述

A 线程必须等待B线程执行完X模块才能执行N模块

接下来我们讲解信号量在一些经典同步问题上的应用

生产者和消费者问题

我们用的是java 中的信号量,这里接单介绍一下他的基本操作:

Semaphore(int );// 构造函数,传入一个整数变量设置value 的值
acquire();// P操作
release(); //V操作

深入介绍请参见API 文档
更多关于java的并发问题请参见

实现目标

生产者消费者问题要实现的目标是:

  • 缓冲区有空位,生产者可以生产
  • 缓冲区有item,消费者可以消费
  • 生产者和消费者互斥访问

生产者和消费者问题java代码

import java.util.concurrent.Semaphore;


public class Main {


    public static void main(String[] args) {
        //TestPV.test();
        TestRW.test();
    }

}
class TestPV{
    public final static int MAX_ITEM = 10;  // 缓冲区最大item数目 
    public static int item =0;  //缓冲区当前最大item
    private static Semaphore mutex = new Semaphore(1);// 互斥信号量
    private static Semaphore full = new Semaphore(0); // 缓冲池满信号
    private static Semaphore empty = new Semaphore(MAX_ITEM);// 缓冲池空信号
    public static Runnable producer = new Runnable() {

        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                try {
                    produceItem();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

        }
    };
    public static Runnable consumer = new Runnable() {

        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                try {
                    consumeItem();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    };
    private static void produceItem() throws InterruptedException {
        empty.acquire();
        mutex.acquire();
        ++item;
        System.out.println("produce a new Item "+item);
        Thread.sleep((long) (5000*Math.random()));
        check();
        mutex.release();
        full.release();
    }
    private static void consumeItem() throws InterruptedException {
        full.acquire();
        mutex.acquire();
        System.out.println("remove a new Item "+item);
        --item;
        Thread.sleep((long) (5000*Math.random()));
        check();
        mutex.release();
        empty.release();
    }
    private static void check() {
        if(!(item>=0 && item <= MAX_ITEM)){
            System.out.println("read or write a wrong item");
        }
    }
    public static void test() {
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}
/*
*一些输出过程
produce a new Item 1
produce a new Item 2
produce a new Item 3
produce a new Item 4
remove a new Item 4
remove a new Item 3
remove a new Item 2
remove a new Item 1
produce a new Item 1
produce a new Item 2
remove a new Item 2
remove a new Item 1
*/

重点看一看 produceItem,和consumeItem 函数的实现

在生产者方,先看一下是否有空位置,对应empty信号量,当他获得访问权后,就互斥的访问缓冲区; 最后释放的时候先将缓冲区访问权释放,然后再释放阻塞在full信号量上的进程(不一定,但是总是增加一个消费者可访问位)

在消费者方,先看一下是否有可访问位(full),然后获取缓冲区操作权最后释放的时候增加一个可制造位。与生产者刚好互补。

读者写者问题

读者写者问题要实现的目标是:

  • 读写互斥
  • 写写互斥
  • 多个读者可同时访问

这里有三种解决手段:

  • 读者优先
  • 写者优先
  • 公平竞争

显然读者优先可能造成写者饥饿,而写者优先可能造成读者饥饿

这里给出一种读者优先的实现,关于其余两种,这篇blog讲的特别精彩:进程同步的经典问题1——读者写者问题(写者优先与公平竞争)

读者写者问题java代码实现

package tmp;

import java.util.concurrent.Semaphore;


public class Main {


    public static void main(String[] args) {
//      TestPV.test();
        TestRW.test();
    }

}

class TestRW{
    //Reader first
    private final static int MAX_MEM = 5;
    private static int[] mem = new int[MAX_MEM]; //写者内存
    private static Semaphore rCntMutex = new Semaphore(1);//读计数互斥
    private static Semaphore wrtMutex = new Semaphore(1);// 写互斥
    private static int rCnt =0; // 读者计数
    private static void write() throws InterruptedException {
        //写者随机写一个单元
        int idx = (int)(Math.random()*(MAX_MEM));
        mem[idx] = (int)(Math.random() * (MAX_MEM*MAX_MEM));
        System.out.printf("I'm writer thread %d, mem[%d] = %d\n",Thread.currentThread().getId(), idx, mem[idx]);
        System.out.println("now the mem is ....");
        for(int i=0; i<MAX_MEM ; ++i){
            System.out.print(mem[i]+" ");
        }
        System.out.println();
        Thread.sleep((long) (5000*Math.random()));
    }
    private static  void read() throws InterruptedException {
        //读者随机读一个单元
        int idx = (int)(Math.random() * (MAX_MEM));
        System.out.printf("I'm reader thread %d, mem[%d] = %d\n",Thread.currentThread().getId(),idx,mem[idx]);
        Thread.sleep((long) (5000*Math.random()));
    }
    static class Reader implements Runnable{

        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                try {
                    rCntMutex.acquire();
                    if(rCnt ==0)//第一个读者,获取写互斥锁
                        wrtMutex.acquire();
                    ++rCnt;
                    rCntMutex.release();//释放读计数变量
                    // read process
                    read();

                    //修改读计数变量
                    rCntMutex.acquire();
                    --rCnt;
                    if(rCnt==0)
                        wrtMutex.release();
                    rCntMutex.release();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }
    static class Writer implements Runnable{
        //写者进程
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                try {
                    wrtMutex.acquire();
                    write();
                    wrtMutex.release();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }

    public static void test() {
        for(int i=0 ; i<2 ; ++i){//创建2个写进程
            new Thread(new Writer()).start();
        }
        for(int i=0 ; i<5 ; ++i)
            new Thread(new Reader()).start();
    }
}

读者优先代码比较好实现,对于读者有一个rCnt 来计数,同时用一个rCntMutex来对 rCnt做互斥访问。 首先访问rCnt若他是第一个读者,则获取wrMutex信号量的获得内存访问权。如果当前没有写者,那读者将访问,后来的写者将等待。

而写者就更好操作了,写者先获取内存操作权,wrMutex实现互斥访问,不过若当前有读者,那他将等待。有写者他也会等待。这里实现了写写,读写互斥。

可是如果读者进程很多,那么写者会造成饥饿。下面是一段允许结果,就恰好造成了写者饥饿

I'm writer thread 10, mem[3] = 24
now the mem is ....
0 0 0 24 0 
I'm writer thread 10, mem[0] = 15
now the mem is ....
15 0 0 24 0 
I'm writer thread 10, mem[2] = 4
now the mem is ....
15 0 4 24 0 
I'm writer thread 10, mem[3] = 13
now the mem is ....
15 0 4 13 0 
I'm writer thread 10, mem[4] = 24
now the mem is ....
15 0 4 13 24 
I'm writer thread 11, mem[3] = 5
now the mem is ....
15 0 4 5 24 
I'm writer thread 11, mem[2] = 11
now the mem is ....
15 0 11 5 24 
I'm writer thread 11, mem[3] = 17
now the mem is ....
15 0 11 17 24 
I'm writer thread 11, mem[1] = 24
now the mem is ....
15 24 11 17 24 
I'm writer thread 11, mem[4] = 19
now the mem is ....
15 24 11 17 19 
I'm writer thread 11, mem[4] = 7
now the mem is ....
15 24 11 17 7 
I'm reader thread 12, mem[3] = 17
I'm reader thread 14, mem[2] = 11
I'm reader thread 16, mem[0] = 15
I'm reader thread 15, mem[3] = 17
I'm reader thread 13, mem[0] = 15
I'm reader thread 16, mem[2] = 11
I'm reader thread 12, mem[3] = 17
I'm reader thread 14, mem[1] = 24
I'm reader thread 12, mem[3] = 17
I'm reader thread 15, mem[1] = 24
I'm reader thread 12, mem[3] = 17
I'm reader thread 13, mem[0] = 15
I'm reader thread 14, mem[2] = 11
I'm reader thread 12, mem[2] = 11
I'm reader thread 13, mem[1] = 24
I'm reader thread 12, mem[2] = 11
I'm reader thread 16, mem[2] = 11
I'm reader thread 15, mem[3] = 17
I'm reader thread 14, mem[0] = 15
I'm reader thread 12, mem[2] = 11
I'm reader thread 15, mem[3] = 17
I'm reader thread 13, mem[4] = 7
I'm reader thread 15, mem[2] = 11
I'm reader thread 16, mem[0] = 15
I'm reader thread 12, mem[4] = 7

后面很长一部分都是读进程。你可以用上面的代码运行试试.

关于写者优先和公平竞争,上面给出的blog中用了一个队列信号来实现等待。任何读者进程进入操作都先申请队列,如果申请不到就等待。写进程优先剩余部分就和读者优先类似了。

哲学家就餐问题

待填坑…

阅读更多

扫码向博主提问

breezeYoung

非学,无以致疑;非问,无以广识
  • 擅长领域:
去开通我的Chat快问
版权声明:本文为博主原创文章,转载请注明出处,欢迎转载。 https://blog.csdn.net/Dylan_Frank/article/details/79965871
上一篇操作系统笔记:(八)进程同步
下一篇操作系统笔记:(十)管程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭