经典同步问题(一)---生产者与消费者问题

时间:2022-04-21 16:41:23
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. 参考:

顶你学堂 操作系统 《第十章 信号量与管程》