【操作系统】经典同步问题:生产者-消费者

考点频率 :★★★★★(PV操作必考经典模型,下午题高频)

难度 :⭐⭐⭐⭐

建议:理解 empty 和 full 两个同步信号量的作用,掌握 P 操作的顺序

1️⃣ 问题描述

生产者-消费者问题是操作系统中最经典的同步问题,描述如下:

  • 若干个生产者 进程往一个共享缓冲区中生产数据(放入产品)
  • 若干个消费者 进程从同一个共享缓冲区中消费数据(取出产品)
  • 缓冲区有大小限制(假设容量为 n)

核心约束

  1. 缓冲区满时,生产者必须等待(不能继续放)
  2. 缓冲区空时,消费者必须等待(不能继续取)
  3. 同一时刻只能有一个进程访问缓冲区(互斥)

这个问题的本质是同步 + 互斥的组合:生产者和消费者之间是同步关系(满则等、空则等),多个生产者和多个消费者之间是互斥关系(同时只能一人操作缓冲区)。

2️⃣ 信号量设计

解决生产者-消费者问题需要三个信号量

信号量 初值 作用
empty n(缓冲区容量) 表示空位的数量。生产者每次放入前 P(empty),消费者每次取出后 V(empty)
full 0 表示已有数据的数量。消费者每次取出前 P(full),生产者每次放入后 V(full)
mutex 1 互斥信号量,保证同一时刻只有一个进程操作缓冲区

其中 emptyfull 是同步信号量(初值0或n),mutex 是互斥信号量(初值1)。

3️⃣ 代码实现

c 复制代码
semaphore empty = n;    // 空位数量,初值为缓冲区大小 n
semaphore full = 0;     // 已有数据数量,初值为0
semaphore mutex = 1;    // 互斥信号量

// 生产者进程
Producer() {
    while (true) {
        // 生产一个产品 item
        
        P(empty);       // 检查是否有空位,没有则阻塞
        P(mutex);       // 进入临界区
        
        // 将 item 放入缓冲区
        
        V(mutex);       // 离开临界区
        V(full);        // 通知消费者:有数据了
    }
}

// 消费者进程
Consumer() {
    while (true) {
        P(full);        // 检查是否有数据,没有则阻塞
        P(mutex);       // 进入临界区
        
        // 从缓冲区取出一个数据 item
        
        V(mutex);       // 离开临界区
        V(empty);       // 通知生产者:有空位了
        
        // 消费数据 item
    }
}

4️⃣ P操作顺序为什么不能颠倒?

在上面的代码中,生产者先执行 P(empty) 再执行 P(mutex),消费者先执行 P(full) 再执行 P(mutex)

如果把顺序颠倒------先 P(mutex) 再 P(empty) 会怎样?

假设缓冲区已满(empty = 0):

  1. 生产者执行 P(mutex) → 成功进入临界区(mutex从1→0)
  2. 生产者执行 P(empty) → empty=0,生产者被阻塞
  3. 此时没有消费者能进入缓冲区(因为 mutex=0,所有消费者被挡在 P(mutex) 外面)
  4. 生产者等着消费者来消费,消费者却进不来

这就是死锁

规则 :在PV操作中,P操作的顺序是:先申请"资源信号量"(同步),再申请"互斥信号量"。V操作的顺序可以反过来。

5️⃣ 信号量值的动态变化

以缓冲区容量 n=3 为例,观察信号量变化:

初始状态empty=3, full=0, mutex=1(缓冲区全空)

操作 empty full 含义
初始 3 0 全空
生产者放入1个 2 1 还有2个空位
生产者再放1个 1 2 还有1个空位
生产者再放1个 0 3 已满
消费者取走1个 1 2 又有1个空位

empty=0 时,生产者再执行 P(empty) 会被阻塞,直到消费者取走数据后执行 V(empty) 唤醒。

6️⃣ 多个生产者和多个消费者的情况

上面的代码可以支持多个生产者进程多个消费者进程同时运行(生产者与生产者之间也需要互斥吗?需要,因为多个生产者可能同时往缓冲区放数据,会破坏数据结构)。

mutex 保证了:

  • 生产者之间互斥
  • 消费者之间互斥
  • 生产者和消费者之间互斥

也就是说,同一时刻只有一个进程(无论是生产者还是消费者)在操作缓冲区。

7️⃣ 经典例题

例题1 :某系统有一个容量为10的缓冲区,用PV操作实现生产者-消费者同步。信号量 empty 初值为10,full 初值为0。若当前 empty=3full=7,则表示( )。

A. 缓冲区中有3个数据,7个空位

B. 缓冲区中有7个数据,3个空位

C. 缓冲区已满

D. 缓冲区为空

解析empty 表示空位数=3,full 表示已有数据数=7。缓冲区容量=10,3+7=10,正常状态。选 B


例题2 :在生产者-消费者问题中,若将生产者代码中的 P(empty)P(mutex) 交换顺序,则可能导致( )。

A. 缓冲区数据丢失

B. 死锁

C. 消费者饥饿

D. 数据不一致

解析 :先 P(mutex) 再 P(empty) 时,若缓冲区已满,生产者会持有 mutex 锁并阻塞在 empty 上,导致消费者无法进入临界区取数据,造成死锁。选 B

8️⃣ 与单缓冲区的区别

对比项 单缓冲区(容量1) 多缓冲区(容量n)
empty初值 1 n
生产者等待条件 缓冲区有数据时等待 缓冲区满时等待
消费者等待条件 缓冲区空时等待 缓冲区空时等待
信号量数量 2个(full, mutex)或2个(empty, full) 3个(empty, full, mutex)

9️⃣ 记忆口诀

生产消费经典题,三个信号量搞定。

empty空位full数,mutex互斥保唯一。

先同步再互斥,P操作顺序别颠倒。

生产P空V满,消费P满V空。

🔟 小测验(评论区对答案)

某系统采用PV操作实现生产者-消费者同步,缓冲区容量为8。当前信号量值为:empty=2, full=6, mutex=1。此时若生产者执行一次 P(empty) 操作,则 empty 的值变为( )。若接着消费者执行一次 V(empty) 操作,则 empty 的值变为( )。

A. 1, 2

B. 1, 1

C. 2, 3

D. 0, 1

🔔 本专栏日更2篇,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容

#软考中级 #软件设计师 #生产者消费者 #PV操作 #进程同步 #操作系统