操作系统之同步与互斥

      系统中可能有着许多并发的进程,在这些进程之间存在这些关系:

    (1)间接制约关系:多个进程之间共享临界资源,例如打印机,全局变量。要保证各个进程互斥的访问临界资源,这种关系叫做进程互斥

    (2)直接制约关系:进程之间存在着某些时序关系,例如进程A需要在进程B执行完后才能执行,这种关系就是进程同步。(linux的内核同步参考:http://blog.csdn.net/lsjseu/article/details/10946119)

      详细总结请看:经典线程同步总结

临界区

    每个进程中,访问临界资源的那段代码叫做临界区。要保证各进程互斥进入临界区。在临界区前面设立一个段进入区,后面加上一段退出区

while(true){

      entry section;//进入区

      critical section;

      exit section;//退出区

}

为了实现互斥访问,有如下准则:

     (1)空闲让进

     (2)忙则等待

     (3)有限等待:保证进程在有限时间进入临界区,不要把lz”饿死“。

     (4)让权等待:等待的时候,让出CPU,不要让CPU空跑。(注意:linux的自旋锁就让CPU空跑了,但是开销很小)

信号量机制

    信号量是一个记录性数据。信号量上提供了两种原子型的操作即P操作,表示申请资源;V操作,表示释放资源。定义如下:

struct sem{

     int value;

     pointer_PCB queue;

}

void p(s)

{

    s.value = s.value - 1;

    if(s.value<0)//小于0,表示当前无资源,自己阻塞到等待队列

         block(s.queue);

}

void v(s)

{

    s.value = s.value + 1;

    if(s.value<=0)//小于等于0,表示当前没有资源,但是我释放了一个资源,可以唤醒等待队列的第一个进程;

         wakeup(s.queue);//如果大于0,表示资源有多余的,进程申请资源肯定能申请,无需唤醒任何进程

}

注:信号量用于互斥的时候,取值只能为1或者0,;但是为了实现同步的时候,就不一定小于1了,具体问题具体分析。多个生产者和消费者我就搞错了。

信号量实现资源互斥访问

   利用P/V操作很容易实现,资源互斥访问。因此,可以设置一个互斥信号量mutex=1(只能是0或者1),每个临界区插入p、v操作代码:

void process()
{
    while(true){
          p(mutex);
          critical section;
          v(mutex);
     }
}

       注:p、v操作把临界区包起来,邪恶了。

信号量实现进程同步

    信号量主要用于进程之间的同步问题。这里总结一个规律:P1进程一定要在P2进程后面执行,则定义一个信号量,在P1实现P操作(后继节点实现P操作,相对于申请资源),P2中实现V操作(前驱节点实现V操作,相对于释放资源)。这也说明进程同步的P、V操作分布在不同的进程中。

      在windows编程里面实现线程同步可用关键段和互斥量,详细参考:

                《秒杀多线程第五篇经典线程同步关键段CS

               《秒杀多线程第七篇经典线程同步互斥量Mutex

例1:p1中的s1一定要在p2的s2执行过之后,根据上面分析,这里之存在一层同步关系,定义一个信号量sync。

sem sync = 0;
void p1()
{
    p(sync)
    s1;
}

void p2()
{
     s2;  
     v(sync);
}

注:在windows编程中,不要用关键段来实现sync信号量,因为关键段不能跨进程使用。最好用事件(Event)或者信号量。事件,互斥量,信号量都是内核对象,可以跨进程使用。


例2;如下图的前驱关系,实现同步

操作系统之同步与互斥_第1张图片

sem f1,f2,f3,f4,f5={0,0,0,0,0};
void s1(){
    ....
    v(f1);//两个后继
    v(f1);
}
void s2(){
    p(f1);//前驱s1
    ....
    v(f2);//两个后继
    v(f2);
}
void s3(){
    p(f1);//前驱s1
    ....
    v(f3);//一个后继
}
void s4(){
    p(f2);//前驱s2
    ....
    v(f4);//一个后继
}
void s5(){
    p(f2);//前驱s2
    ....
    v(f5);//一个后继
}
void s6(){
    p(f3);//前驱s3
    p(f4);//前驱s4
    p(f5);//前驱s5
}
注:有前驱和后继关系就要申请信号量,实现进程同步。



生产者-消费者问题

    windows编程线程同步请参考:

             《秒杀多线程第六篇经典线程同步事件Event

            《秒杀多线程第八篇经典线程同步信号量Semaphore

(1)一个生产者、一个消费者和一个缓冲区

     这里面有一个同步和互斥问题。同步:消费者要等到缓冲区满后才能消费,即生产者是消费者前驱。而生产者要等消费者要等缓冲区空后才能生产,即消费者是生产者前驱。互斥:消费者和生产者不能同时访问缓冲区。

操作系统之同步与互斥_第2张图片

       生产者和消费者问题windows编程,参考:生产者-消费者问题

按照上面的分析分别设定如下信号量:

sem empty = 1;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
producer()
{
    while(true){
        p(empty);//消费者通知生产者
        p(mutex);//保证buffer互斥访问
        生产者生产放进buffer;
        v(mutex);
        v(full);//通知消费者可以消费
    }
}
consumer()
{
     while(true){    
        p(full);//由生产者通知
        p(mutex);//保证buffer互斥访问
        从buffer里面消费;
        v(mutex);
        v(empty);//通知生产者可生产
     }
}


(2)多个生产者、多个消费者和多个缓冲区

      一样的思路,多了两个变量in和out分别表示放进那个buffer缓冲区和从哪个缓冲区取。下面程序原本我忽略了一个问题,我是将empty=1,这就不对了。应该是这样的sem empty = MAX(MAX表示你的缓冲区个数)。如果还是初始化为1,每次只能限制生产一个产品,而且还有等待消费者把这个产品消费掉后,生产者才会去生产,而且最终谁去生产,就看谁先抢到empty这个信号量了。在windows编程里面可以用下语句:

Empty = CreateSemaphore(NULL, 4, 4, NULL);  //前一个值表示,初识值为4,而且最大值可以为4。
Full  = CreateSemaphore(NULL, 0, 4, NULL);  //初识为0,最大值可以达到0。

之前看这个程序一直纳闷,现在彻底明白了,彻底懂了,更正如下:

const int MAX = 4;//缓冲区的个数
sem empty = MAX ;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
int in = out = 0;
producer()
{
    while(true){
        p(empty);//消费者通知生产者
        p(mutex);//保证buffer互斥访问
        in = (in + 1)%MAX;
        生产者生产放进buffer[in];
        v(mutex);
        v(full);//通知消费者可以消费
    }
}
consumer()
{
     while(true){    
        p(full);//由生产者通知
        p(mutex);//保证buffer互斥访问
        out = (out + 1)%MAX;
        从buffer[out]里面消费;
        v(mutex);
        v(empty);//通知生产者可生产
     }
}



下面考虑这样一个问题:

const int MAX = 4;//缓冲区的数量
sem empty = 1;//生产者和消费者同步
sem full = 0;//生产者和消费这同步
sem mutex = 1;//缓冲区互斥
int in = out = 0;
producer()
{
    while(true){
        p(mutex);//交换
        p(empty);//1
        in =(in + 1)%MAX;
        生产者生产放进buffer[in];
        v(mutex);
        v(full);
    }
}
consumer()
{
     while(true){    
        p(mutex);//2
        p(full);//
        out = (out + 1)%MAX;
        从buffer[out]里面消费;
        v(mutex);
        v(empty);
     }
}

说明:上面这段程序交换了p(mutex)和p(empty),带来什么后果呢?我们假设生产者生产跑得比较快,当生产者生产了一个商品后,消费者还没启动。当生产者进入语句//1,则由于buffer里面的东西没有被消费者干掉而等在这里。而此时,由于生产者进入临界区了,消费只能在外面等,无法进入临界区消费,只能在//2处等。这出现死锁现象。相当于生产者自己把门关了,却等着消费者来买东西,进不来啊,人家。


哲学家进餐问题

     五个哲学家,坐在一个桌子上,五支筷子,五个碗。则有下面程序:

操作系统之同步与互斥_第3张图片

sem chopstick[5]={1,1,1,1,1}
void philosopher(int i)
{
     while(true){
          p(chopstick[i]);//取左边的筷子
          p(chopstick[i+1]);//取右边的筷子
          eating;//吃饭
          v(chopstick[i]);
          v(chopstick[i+1]);
          thinking;//思考
     }
}

      会出现这么一瞬间:当所有的哲学家都拿到了左边的筷子,想去拿右边的筷子时候,跪了,被人占了。于是大家就只能死等了。

解决方法:

(1)只允许四个哲学家坐在桌边;

(2)当且仅当哲学家可以拿起两边的筷子时,才允许他们拿,这看谁抢得快,抢不到就等吧

(3)奇数号的哲学家必须首先抓起左边的筷子,偶数相反。


读者与写者问题

        临界区允许多个读者同时进入,而写者只能互斥进入。这类问题叫做“读者-写者问题”。两种处理方案,一种是读者优先,另一种是写者优先。它们之间的区别在于,当写者提出请求后,是否允许读者进入。如果允许进入,表示读者优先,否则写者优先。

      读者-写者问题windows编程,参考:读者-写者问题

(1)错误的方法

      这种实现方法只能保证每次进去一个人,要么读者要么写者。

//这种实现方案
sem mutex = 1;
p(mutex);//只能保证每次只能进入一个读者读(写)文件
读者或写者可进入临界区
v(mutex);


注:下面的程序当写者有多个的时候,不要简单的将信号量赋成1,不要犯同样的错误。

(2)读者优先方法

       就是不管有没有写者,读者都可以进去读。感觉有点乱,下面程序是读者还是要等待写者写完了以后才能进去。

//下面实现一种读者优先的方案:
int   readerCnt = 0;//记录当前的多少读者
sem   hasreader = 0;//读者与写者之间的同步
sem   mutex = 1;//reader变量互斥访问
Reader();//读者可以任意进入,读者来了,写者滚蛋
{
   while(true){  
      p(mutex)
      readerCnt++;//来一个读者,加一个读者
      if(readerCnt>0)
          p(hasReader);//如果此时,里面有写者,则阻塞读者,无写者读者可进入
      v(mutex);
      读者开始读书;
      p(mutex)
      readerCnt--;//读者读完离开
      if(readerCnt==0)
	      v(hasReader);//没有读者的时候,才唤醒读者
      v(mutex);
   }
}
Writer()
{
   while(true){
      P(hasReader);
      写者写书中...
      V(hasReader); 
   }
}
上面的程序:如果一个写者已进入临界区且有n个读者正在等待,则只有一个读者在等待hasReader这个信号量排队。其他n-1个读者都在mutex上排队;另外,一个写者V(hasReader)以后,既可以允许一个正在等待的写者进入,也可以允许若干个正在等待的读者进入。


(3)写者优先方法

     看了一个大牛写的写者优先思想:如果当前有写者在等待,则一定是写着先进入。读者进入的时候,要进行如下判断:一是否有写者正在写,这种情况肯定是不能进去的,只能在等待队列里面等;二是要判断写者等待的数量是否大于0,如果当前有写者在等待,还得继续等,因为写者优先嘛。

      总结起来读者进入的条件:一是当前进入临界区一定是读者而且此时没有写者等等进入临界区,有写者正在写肯定是不能进入;二是有写者如果在等临界资源,只能给他让道;三是如果读者正在读的时候,来了一批写者,那么此时后面进来的读者也不能进入了,只能等当前正在读的读者进行完,然后这批写者进入写完,再才轮到这些读者,可怜啊。

     注:读者优先反过来。


(4)读者和写者一样优先权

      但是能保证读者多次进入,就是说,当前里面是读者,读者可以继续进入,但是写者不可以进入。写者只能互斥的进入。这里面存在两种前驱关系:

前驱关系1:没有写者,读者才能进入;

前驱关系2:没有读者,写者才能进入:

综上,应该是两个信号量,而不是一个信号量。以下代码:

sem mutex = 1;
p(mutex);//只能保证每次只能进入一个读者读(写)文件
v(mutex);

//下面实现一种读者优先权一样的方案:
int   readerCnt = 0;//记录当前的多少读者

sem   hasReader = 0;//读者,先让读者进入
sem   hasWriter = 1;//写者

sem   mutex = 1;//reader变量互斥访问

Reader();
{
   while(true){ 
      p(hasWriter) 
      p(mutex)
      readerCnt++;//来一个读者,加一个读者
      if(readerCnt>0)
          p(hasReader);
      v(mutex);
      读者开始读书;
      p(mutex)
      readerCnt--;//读者读完离开
      if(readerCnt==0)
	      v(hasReader);//唤醒读者
      v(mutex);
   }
}
Writer()
{
   while(true){
      P(hasReader);
      写者写书中...
      V(hasWriter); 
   }
}
      利用读写锁解决读写者问题参考:读写锁

你可能感兴趣的:(操作系统)