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

原创 2018年04月16日 20:23:14

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

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

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

这里写图片描述

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中用了一个队列信号来实现等待。任何读者进程进入操作都先申请队列,如果申请不到就等待。写进程优先剩余部分就和读者优先类似了。

哲学家就餐问题

待填坑…

版权声明:本文为博主原创文章,转载请注明出处,欢迎转载。 https://blog.csdn.net/Dylan_Frank/article/details/79965871

apiDoc自动化文档同步工具

学习该课程,可以通过简单的配置,一个命令就能自动生成接口文档,直接使用浏览器可以查看,实时更新。方便前后端开发人员查看,生成的接口文档简单明了,抛弃传统的手工编写文档,而且适合大部分主流语言java、nodejs、python、perl、php等,且与代码无任何耦合侵入性。
  • 2016年07月07日 08:42

操作系统--信号量经典同步问题之读者优先问题

优先问题实际上是指用信号量来优先级抢占。这里读者优先有以下两个内涵: 1. 当读者线程正在访问临界区时,其他的读者线程可以优于写者线程访问临界区,只有所有的读者线程都执行完毕后,写者线程才能访问临界区...
  • gaoqiangxjtu
  • gaoqiangxjtu
  • 2016-12-19 10:28:47
  • 351

操作系统--信号量经典同步问题之写者优先问题

写者优先比读者优先要复杂一些,写者优先有以下两个内涵: 1. 当写者线程获得临界区的访问权限时, 其他写者线程不要需要要优先于读者线程获得临界区的访问权限,只有当所有的写者线程都执行后,读者线程才能...
  • gaoqiangxjtu
  • gaoqiangxjtu
  • 2016-12-19 13:55:00
  • 624

计算机操作系统 2.5对经典进程的同步问题 的简单解释

首先简单说明以下P.V.操作的作用P操作:信号量– –; V操作:信号量++;生产者—消费者问题:empty:表示空缓冲区的数目,其初值为缓冲池的大小n,表示消费者已把缓冲池中全部产品取走,有n个...
  • qq_40712616
  • qq_40712616
  • 2017-12-27 21:12:59
  • 72

计算机操作系统笔记(4)--进程管理之进程同步

理解临界资源和临界区的概念,熟练掌握利用信号量机制解决进程同步问题。对多个相关进程在执行次序上进行协调,使并发执行的诸进进程之间能有效地共享资源和相互合作,从而使用程序的执行好具有可再现性。...
  • li2008kui
  • li2008kui
  • 2016-11-21 17:18:16
  • 756

计算机操作系统笔记(5)--进程管理之经典进程的同步问题

本文分别介绍三个经典进程的同步问题,即“生产者-消费者问题”、“哲学家进餐问题”以及“读者-写者问题”。...
  • li2008kui
  • li2008kui
  • 2016-11-22 09:46:07
  • 1434

操作系统:经典进程同步问题(2)哲学家进餐问题

知识总结: 互斥模式:互斥信号量赋初值一定是1 下面这种解决方法: 如果每个科学家都拿起相同方向的意志筷子就会导致每个人只能得到一个筷子,造成死锁! ...
  • Zzwtyds
  • Zzwtyds
  • 2017-11-05 16:14:56
  • 393

经典进程同步问题:吸烟者问题

问题描述 假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟 并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟 者中,第一个拥有烟草、第二个拥有纸...
  • MeredithRowe
  • MeredithRowe
  • 2017-04-18 09:50:21
  • 1817

操作系统(经典进程同步问题)之写者优先

一个数据文件或记录可以被多个进程共享使用,我们将读文件的进程称为reader,写文件的进程成为writer。允许对个进程同属进行读取一个共享对象,因此读操作不会造成数据数据文件的混乱,但不允许read...
  • cbcxvbfxgb
  • cbcxvbfxgb
  • 2017-03-27 17:57:20
  • 1818

操作系统之——进程同步经典问题

点击打开链接生产者消费者问题 一个生产者,一个消费者,公用一个缓冲区 定义两个同步信号量: empty——表示缓冲区是否为空,初值为1。 full——表示缓冲区中是否为满,初值为0。 生...
  • qq_26888929
  • qq_26888929
  • 2016-12-05 21:20:53
  • 989
收藏助手
不良信息举报
您举报文章:操作系统笔记:(九)信号量与经典同步问题
举报原因:
原因补充:

(最多只允许输入30个字)