经典进程同步问题

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函数),五个哲学家将全部被送入等待链表被阻塞,出现死锁。

原文地址:https://www.cnblogs.com/Joezzz/p/9757677.html