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