• 经典进程同步问题


    1.信号量(semaphore)

    1.1概念

      信号量是一个计数器,常用来实现进程间的同步和互斥

    1.2允许的两个操作

    1)原子操作:原子操作是不会被中断的操作,要么就不干,要是干了,它就不会被任何东西打断

    2)以下两个均为原子操作

    3)down操作P操作):将信号量的值值减1;当信号量等于0时,再次down操作会让之后的进程进入等待(阻塞态)

    void wait(semaphore S)
    {
        --S.value;
        if (S.value<0)
        {
            add this process to S.L;//进程链表L,记录所有等待该资源的进程
            block(S.L);
        }
    }

    4)up操作V操作):将信号量的值加1;特别的,对一个有进程在其上等待的信号量执行一次up操作后,唤醒在其上等待的其中一个进程进入就绪态

    void signal(semaphore S)
    {
        ++S.value;
        if (S.value<=0)
        {
            remove a process P to S.L;//进程链表L,记录所有等待该资源的进程
            wakeup(P);
        }
    }

    1.3信号值所代表的含义

    1)当信号量S大于0时表示可用的临界资源数,当等于0时,表示刚好用完;

    2)标准定义,信号量只能为非负整数,但是,当信号量S小于0时,也有其意义,这个负值的绝对值表示系统中因申请该类资源而被阻塞的进程数目

    举例:有两个某类资源,四个进程A、B、C、D要用该类资源;

       最开始S=2,当A进入,S=1,当B进入,S=0,表明该类资源刚好用完;

       当C进入时S=-1,表明有一个进程被阻塞了,D进入,S=-2,表明有两个进程被阻塞了;

       当A用完该类资源时,进行V操作,S=-1,释放该类资源,因为S<=0,表明有进程阻塞在该类资源上,于是唤醒C,C进入就绪态,C发现有一个资源可以用,然后直接转换到运行态,使用该资源;

               当B用完该类资源时,进行V操作,S=-0,释放该类资源,因为S<=0,表明有进程阻塞在该类资源上,于是唤醒D,D进入就绪态,D发现有一个资源可以用,然后直接转换到运行态,使用该资源;

    1.4信号量初值与其作用

    1)如果初值为0,用来实现进程间的同步

    2)如果初值为1,用来实现进程间互斥访问

    3)如果初值为N(N一般大于1),用来限制并发数目

    1.5互斥量

    1)信号量的一个特殊版本

    2)值为0或者1,0为加锁状态,1为不加锁状态

    3)当互斥量处于锁住状态时,任何试图再次加锁的行为都将被阻塞

    2.生产者-消费者问题

    2.1问题描述

      两个进程共享一个大小为n的缓冲区,其中一个是生产者,生产数据并将数据放入缓冲区;另一个是消费者,从缓冲区中取出数据。由于缓冲区是临界资源,如何实现缓冲区的互斥访问和生产者、消费者之间的同步

    2.2分析

    使用三个信号量:

    1.full,记录缓冲区满槽数目,用于同步,初值为0;

    2.mutex,用于互斥,初值为1;

    3.empty,记录缓冲区空槽数目,用于同步,初值为n;

    2.3代码

    semaphore full = 0;     //缓冲区满槽数目,同步量
    semaphore mutex = 1;    //互斥量
    semaphore empty = n;    //缓冲区空槽数目
    
    void producer()
    {
        while (1)
        {
            produce an item in nextp;
            down(empty);                //down(empty)一定要在down(mutex)前
            down(mutex);
            add nextp to buffer;
            up(mutex);
            up(full);
        }
    }
    
    void consumer()
    {
        while (1)
        {
            down(full);                    //down(full)一定要在down(mutex)前
            down(mutex);
            remove an item from buffer;
            up(mutex);
            up(empty);
        }
    }

    2.4代码解读

    2.4.1同步

    最开始缓冲区里没有数据,应该达到的效果是:消费者被阻塞,一旦生产者将数据送入缓冲区,消费者被唤醒去取数据,我们来看看是如何实现的:

    1)消费者down(full),full=-1,根据down操作的定义(见上面代码wait函数),full<0,消费者将被送入等待链表被阻塞;

    2)某时刻生产者想生产,“produce an item in nextp; down(empty); down(mutex); add nextp to buffer;up(mutex);”,一气呵成地走下来,然后up(full),full=0,根据up操作的定义(见上面代码signal函数),full<=0,消费者从等待链表中移除,然后被唤醒,变成就绪态,然后生产者彻底走完这次流程,消费者就变成运行态开始继续往下走了

    反之,缓冲区满时,生产者应该被阻塞,一旦消费者取走数据,生产者才被唤醒去把数据放入缓冲区,具体实现过程类似,此处不再赘述。

    2.4.2互斥

    讨论互斥时,为了方便,假设现在缓冲区既不满也不空;

    1)某时刻生产者想生产,“produce an item in nextp; down(empty); down(mutex);”,走到这,mutex由1变为0;

    2)此时如果消费者想进来是进不来的,因为消费者“remove an item from buffer;”之前也需要down(mutex),这样的话,mutex就变成了-1,小于0,根据down操作的定义(见上面代码wait函数),mutex<0,消费者将被送入等待链表被阻塞

    3)所以生产者可以安心地向下走,“add nextp to buffer;”,直到up(mutex)后,mutex=0,根据up操作的定义(见上面代码signal函数),mutex<=0,消费者从等待链表中移除,然后被唤醒,变成就绪态;

    4)然后生产者彻底走完这次流程,消费者就变成运行态开始往下走。

    2.4.3如果生产者和消费者把down(empty)和down(full)在down(mutex)后面,会出现问题:

    1)当生产者将缓冲区放满了,消费者还没有工作,然后生产者又想生产,生产者先down(mutex)再 down(empty),mutex=0,empty=-1,根据down操作的定义(见上面代码wait函数),empty<0,生产者将被送入等待链表被阻塞,等待消费者出马取走数据;

    2)然后过了一会消费者来了,先down(mutex),mutex=-1,根据down操作的定义(见上面代码wait函数),mutex<0,消费者也将被送入等待链表被阻塞;

    3)于是双方都被阻塞,就陷入了无休止的等待。

    3.读者-写者问题

    3.1问题描述

       有读者和写者两组并发程序,共读写一个文件,允许多个读者同时读,在进行写操作时不允许读操作和任何其他的写操作。

    3.2分析

    需要三个互斥量

    1)读者和写者互斥,需要一个互斥量readAndWrite

    2)写者和写者互斥,可以借助readAndWrite

    3)读者和读者不互斥,写者要等读者为0的时候才能写,所以需要一个readCount来记录读者数,并且要保证readCount在更新时不被另一个读者更新,所以还需要一个互斥量mutexReadCount

    4)为了保证写请求的公平,需要一个互斥量gongpingWrite

    3.3代码

     1 int readCount = 0;                //记录正在读的读者数
     2 semaphore mutexReadCount = 1;        //用于保证更新count变量时的互斥
     3 semaphore readAndWrite = 1;    //用于保证读和写的互斥
     4 semaphore gongpingWrite = 1;        //用于保证写和读的相对公平
     5 void writer()
     6 {
     7     while (1)
     8     {
     9         down(gongpingWrite);            //有写请求时,在写请求之后的读请求被禁止
    10         down(readAndWrite);        //读和写互斥
    11         writing;                //写操作
    12         up(readAndWrite);        //读和写互斥
    13         up(gongPingWrite);                //恢复对共享文件的访问
    14     }
    15 }
    16 
    17 void reader()
    18 {
    19     while (1)
    20     {
    21         down(gongpingWrite);            //有写请求时,在写请求之后的读请求被禁止
    22         down(mutexCount);            //互斥访问count变量
    23         if (readCount == 0)            //第一个读进程读共享文件时,写被禁止
    24             down(readAndWrite);
    25         ++readCount;
    26         up(mutexReadCount);                //互斥访问count变量
    27         up(gongpingWrite);                //恢复对共享文件的访问
    28         reading;                //读操作
    29         down(mutexReadCount);            //互斥访问readCount变量
    30         --readCount;
    31         if (readCount == 0)            //所有读者读完,写才被允许
    32             up(readAndWrite);
    33         up(mutexReadCount);                //互斥访问count变量
    34     }
    35 }

    3.3代码解释

    结合注释来看

    1)写者要等读者数量为0的时候才能写:23行和31行

    2)保证count在更新时不被另一个读者更新:22、26、29、33

    3)保证写请求的公平:9、13、21、27

      为什么说保证写请求的公平,如果没有这四行,假设这么一种情况,当一个读者在进行读操作时,其后连续不断的读者的读请求都将被允许,而写者的写请求将被拒绝,陷入长时间等待,等到没有新读者的请求也没有正在写的读者。这就是一种对写请求的不公平。

      这四行就是来解决这个问题的:有一个读者正在进行读操作时,这时有一个写者发起写请求,它要等待count=0才能进行写操作,同时它执行第9行down(gongpingWrite),由于第21行的down(gongpingWrite),这就等于禁止了后续读者的读请求,它只需要等这个正在进行读操作的读者出来就可以了

    4.哲学家就餐问题

    4.1问题描述

      一张圆桌上坐着5个哲学家,每个哲学家之间摆了一根筷子,筷子之间是一碗米饭;哲学家要么思考要么进餐;哲学家思考不影响他人;当哲学家进餐时,试图拿起左右两边的筷子(一根一根地拿),如果筷子在他人手里,则需等待,只有拿到两根筷子才能进餐,进餐完后又进行思考。

    4.2分析

    1)设置取筷子的互斥量mutex;

    2)设置五个取左还是右筷子的互斥量chopstick[5];

    2)对哲学家 i 编号0~4,哲学家i左边的筷子编号为 i,右边的筷子编号(i+1)%5;

    4.3代码

    semaphore mutex = 1;
    semaphore chopstick[5] = { 1,1,1,1,1 };
    
    void P(i)
    {
        while (1)
        {
            down(mutex);    //锁住取左筷子和右筷子的动作
            down(chopstick[i]);
            down(chopstick[(i + 1) % 5]);
            up(mutex);        //解锁,现在其他人可以试着取左筷子和右筷子了,注意:是“试着”
            eat;
            up(chopstick[i]);
            up(chopstick[(i + 1) % 5]);
            think;
        }
    }

     4.4代码解读

     1)使用互斥量mutex的原因:防止死锁

      如果没有mutex,当五个哲学家都想要进餐,分别拿起他们左边的筷子,均执行down(chopstick[i]),chopstick[i]均变为0,筷子被拿光,当他们拿右边的筷子时,执行down(chopstick[(i+1)%5]),chopstick[i]均变为-1,小于0,根据down操作的定义(见上面代码wait函数),五个哲学家将全部被送入等待链表被阻塞,出现死锁。

  • 相关阅读:
    2019-2020-1 20175316 《信息安全系统设计基础》第5周学习总结
    2019-2020-1 20175316 《信息安全系统设计基础》第4周学习总结
    2019-2020-1 20175316 《信息安全系统设计基础》第3周学习总结
    第06组 Alpha冲刺(4/6)
    第06组 Alpha冲刺(3/6)
    第06组 Alpha冲刺(2/6)
    第06组 Alpha冲刺(1/6)
    第06组 团队Git现场编程实战
    团队项目-需求分析报告
    团队项目-选题报告
  • 原文地址:https://www.cnblogs.com/Joezzz/p/9757677.html
Copyright © 2020-2023  润新知