信号量与管程实现生产者消费者模型

生产者消费者模式是实际应用中常见的场景,一个或多个生产者线程往缓冲区添加数据,一个或多个消费者线程从缓冲区取数据,生产者消费者模型有这样几个正确性要求:

1.缓冲区是共享变量,一般由数组或者链表实现,是线程不安全的,同一时刻,只能有一个线程操作缓冲区(要求互斥);

2.当发现缓冲区满后,生产者必须等待消费者(要求同步);

3.缓冲区空后,消费者必须等待生产者(要求同步)

基于信号量解决这个模型,设计思路如下:

1.buffer缓冲区,大小n

2.buffer是线程不安全的共享变量,需要在临界区进行,mutex=new Semaphore(1)

3.控制buffer满了生产者等待消费者的计数信号量,初始值表明当前生产者可以往buffer中添加n个数据

       fullSemp=new Semaphore(n),生产者生产一个元素该值-1,消费者添加元素该值+1

4.控制buffer空了消费者等待生产者的计数信号量,初始值表明当前消费者可以从buffer中取出0个数据

       emptySemp=new Semaphore(0),生产者生产一个元素该值+1,消费者添加元素该值-1

5.buffer设计:

       采用数组方式:基于下标插入和获取元素

       putIndex=0;插入元素的下标,生产者一直往后put,到最大时,归0

       takeIndex=0;获取元素的下标,消费者一直往后take,到最大时,归0

代码如下:

producer(){

       fullSemp-->P();//生产者可生产个数-1,如果减之后<0,阻塞

       mutex-->P();//buffer数组,属于共享数据,添加数据是线程不安全操作,需要互斥

       insertToBuffer(new e);

       mutex-->V();

       emptySemp-->V();//消费者可消费个数+1,如果当前<=0,说明有消费者在阻塞,会自动唤醒一个消费者

}

consumer(){

       emptySemp-->P();消费者可消费个数-1,如果减之后<0,阻塞

       mutex-->P();//buffer数组,属于共享数据,获取数据是线程不安全操作,需要互斥

       takeFrombuffer();

       mutex-->V();

       fullSemp-->V();//生产者可生产个数+1,如果当前<=0,说明有生产者在阻塞,会自动唤醒一个生产者

}

//为什么说buffer的插入读取操作是线程不安全的呢?因为涉及到共享变量takeIndex和putIndex的非原子性访问

void insertToBuffer(e){

       buffer[putIndex++]=e;

       if(putIndex==n){

              putIndex=0;

       }

}

e takeFrombuffer(){

       e=buffer[takeIndex++];

       buffer[takeIndex]==null;

       if(takeIndex==n){

              takeIndex=0;

       }

       return e;

}

考虑一个问题,producer里的两个P操作能调换顺序么?答案是不能

试想一下,如果调换顺序,假如现在缓冲区已经满了,再调用producer,就会先拿到mutex信号量,然后阻塞在fullSemp信号量上等待消费者,此时再调用consumer,就会阻塞在mutex信号量上等待生产者,产生了死锁

基于管程解决这个模型,设计思路如下:

1.buffer缓冲区,大小n,依然用数组实现

2.int count=0;//表示缓冲区中现存元素数,初始为0

3.管程的互斥锁Lock lock;

4.管程的条件变量,Condition notFull,表示缓冲区未满的条件,当缓冲区满时,条件转为不满足,生产者阻塞

5.管程的条件变量,Condition notEmpty,表示缓冲区未空的条件,当缓冲区空时,条件转为不满足,消费者阻塞

代码如下: 

producer(){

      lock-->Acquire();//管程的生产消费逻辑是进入就加锁,这和上面信号量的方式有所区别

      while(count==bufferSize){

            notFull.wait(&lock);//缓冲区满,notFull条件转为不满足,执行等待,释放互斥锁

      }

      buffer[count++]=new e;

      notEmpty.signal();//如果有消费者在等待,会唤醒其中一个

      lock-->Release();

}

consumer(){

      lock-->Acquire();//管程的生产消费逻辑是进入就加锁,这和上面信号量的方式有所区别

      while(count==0){

            notEmpty.wait(&lock);//缓冲区空,notEmpty条件转为不满足,执行等待,释放互斥锁

      }

      e=buffer[count--];

      buffer[count]=null;

      notFull.signal();//如果有生产者在等待,会唤醒其中一个

      lock-->Release();

}

还记得我在上一篇文章信号量与管程原理留的一个问题吗?我把它贴过来

Condition::Wait(lock){

              numWaiting++;//等待的线程数增1

              add this thread t to waitQ;//线程加到等待队列中

              release(lock);//wait操作一定是获得锁的情况下执行的,这里要释放掉已经拿到的锁,让其他线程可以执行

              schedule();//这里可以理解为留一定的时间让其他线程去执行

              acquire(lock);//为啥这里还要再获取锁呢?

}

为啥释放锁后,后面又跟个获取锁?单看wait方法确实难以理解,我们把wait的内容带入producer方法中

producer(){

      lock-->Acquire();//管程的生产消费逻辑是进入就加锁,这和上面信号量的方式有所区别

      while(count==bufferSize){

              //notFull.wait(&lock);//缓冲区满,notFull条件转为不满足,执行等待,释放互斥锁

              numWaiting++;//等待的线程数增1

              add this thread t to waitQ;//线程加到等待队列中

              release(lock);//wait操作一定是获得锁的情况下执行的,这里要释放掉已经拿到的锁,让其他线程可以执行

              schedule();//这里可以理解为留一定的时间让其他线程去执行

              acquire(lock);//试想下,没有这句会怎样,当前线程已经释放掉锁了,然后留了一定的时间给其他线程执行,然后再执行while判断,此时不论while是否满足,后续操作都需要释放锁,锁已经释放掉了,哪来的锁再去释放?所以必须再视图获取下锁,成功后才能继续往下进行

             再换个角度想一想,这里难理解的原因是,释放锁操作在加锁的前面,这和我们平时使用锁操作的步骤是相反的,但无论怎样,大家一定知道加锁与释放锁一定是成对出现的,不管谁先谁后

      }

      buffer[count++]=new e;

      notEmpty.signal();//如果有消费者在等待,会唤醒其中一个

      lock-->Release();

}

PS:如果有不足之处,欢迎指出;如果解决了你的疑惑,就点个赞吧o(* ̄︶ ̄*)o

猜你喜欢

转载自blog.csdn.net/wb_snail/article/details/105426844