1. 问题描述
有界缓冲区的生产者-消费者问题描述:
有一个或多个生产者线程生产某种类型的数据,并放置在缓冲区中;有一个或多个消费者线程从缓冲区中取数据并进行处理,每次取一项;在任何时候只能有一个生产者或消费者可访问缓冲区;当缓存已满时,生产者不会继续向其中添加数据;当缓存已空时,消费者不会从中移走数据。
2. 信号量实现:
2.1 问题分析:
任何时刻只能有一个线程操作缓冲区,说明所有线程是互斥关系
缓冲区空时,消费者必须等待生产者;缓冲区满时,生产者必须等待消费者。说明生产者和消费者线程也是同步关系
用信号量描述每个约束:
使用二进制信号量mutex作为互斥信号量,用于控制互斥访问缓冲池,初值为1
使用计数信号量fullBuffers记录当前缓冲池中“满”缓冲区,初值为 0
使用计数信号量emptyBuffers记录当前缓冲池中“空”缓冲区数,初值为n
2.2 伪代码实现
Class BounderBuffer
{
mutex = new Semaphore(1);
fullBuffers = new Semaphore(0);
emptyBuffers = new Semaphore(n);
}
BounderBuffer::Produce(c)
{
emptyBuffers->P();
mutex->P();
add c to buffer;
mutex->V();
fullBuffers->V();
}
BounderBuffer::Consume(c)
{
fullBuffers->P();
mutex->P();
consume one from buffer; c = one;
mutex->V();
emptyBuffers->V();
}
2.3 注意:
(1)emptyBuffers的P操作必须在mutex的P操作前面,否则如果当前缓冲池中没有“空”缓冲区,当生产者进入了临界区并等在P操作处,消费者由于生产者占着临界区不能申请临界区消费数据,导致两者死锁永远等待下去
(2)当生产者生产了一个缓冲区,不能忘记调用fullBuffers的V操作释放它
(3)同(1)fullBuffers的P操作必须在mutex的P操作前面
(4)从这里可以看出使用信号量实现进程的同步比较困难,要求程序员能很好的了解问题并很好的运用信号量机制,否则可能因为信号量的P操作顺序弄错了或忘记释放信号量导致死锁或其他错误。
3. 管程实现
管程引进的目的就是改进信号量在处理临界区的时候的一些麻烦,它分离了互斥和条件同步。
3.1 伪代码实现
class BounderBuffer{
...
Lock lock;
int count = 0; 可消费的数量
Condition notFull,notEmpty;
}
BounderBuffer::Produce(c)
{
lock->Acquire(); //一进入管程函数就要加锁进行互斥操作,这是由管程定义决定的:线程进入管程的时候只有一个线程能进去。每一个管程函数的代码都要互斥的调用。
while(count == n)
notFull.Wait(&lock);
Add c to the buffer;
count++;
notEmpty.Signal();
lock->Release(); //退出管程,解锁
}
BounderBuffer::Consume(c)
{
lock->Acquire();
while(count == 0)
notEmpty.Wait(&lock);
consume one from buffer; c = one;
count--;
notFull.Signal();
lock->Release();
}
3.2 注意:
对比使用信号量解决生产者消费者的问题,生产者必须首先判断buffer是否有可写的数据,然后再申请锁进入临界区。如果先申请了临界区,而不满足有空缓冲区这个条件,其他线程也不能获取互斥锁从而造成死锁。而这里通过管程实现生产者和消费者的顺序是先使用锁进入临界区,然后再判断是否为空或慢。使用管程之所以不会死锁的原因是:当条件不满足Wait操作可以释放互斥锁。
4. 参考:
顶你学堂 操作系统 《第十章 信号量与管程》