操作系统硬核干货:彻底搞懂“生产者-消费者问题”的三种解法

张开发
2026/4/12 19:37:54 15 分钟阅读

分享文章

操作系统硬核干货:彻底搞懂“生产者-消费者问题”的三种解法
操作系统硬核干货彻底搞懂“生产者-消费者问题”的三种解法在学习操作系统时生产者-消费者问题Producer-Consumer Problem绝对是绕不开的一座大山。它不仅是进程同步的经典案例更是大厂面试和期末考试的常客。简单来说这个问题描述的是有一群生产者在生产数据放入缓冲区一群消费者从缓冲区取数据去处理。如何保证大家配合默契不撞车、不空跑今天我们就来深度解析解决这个问题的三种主流方法记录型信号量、AND型信号量以及管程。 核心难点我们要解决什么在动手写代码之前我们先明确三个必须遵守的规则互斥性缓冲区是公共资源同一时间只能有一个人生产者或消费者操作它不能“撞车”。同步性空缓冲区满了生产者得停下不能硬塞溢出。同步性满缓冲区空了消费者得停下不能硬取下溢。️ 方法一记录型信号量最经典这是教科书中最常见的解法。我们需要定义三个信号量mutex互斥信号量初值为1。用来保护缓冲区保证同一时间只有一个进程在访问。empty资源信号量初值为n缓冲区大小。表示空缓冲区的数量。full资源信号量初值为0。表示满缓冲区也就是产品的数量。实现逻辑生产者先申请空位P操作empty再申请锁P操作mutex放入产品释放锁V操作mutex最后告诉别人多了一个产品V操作full。消费者先申请产品P操作full再申请锁P操作mutex取走产品释放锁V操作mutex最后告诉别人多了一个空位V操作empty。伪代码实现// 全局变量定义 int in 0, out 0; // 缓冲区指针 item buffer[n]; // 共享缓冲区 semaphore mutex 1; // 互斥信号量 semaphore empty n; // 空缓冲区数量 semaphore full 0; // 满缓冲区数量 // 生产者进程 void producer() { do { producer an item nextp; // 1. 生产一个产品 wait(empty); // 2. 申请一个空缓冲区P(empty) wait(mutex); // 3. 申请进入临界区P(mutex) /* --- 临界区开始 --- */ buffer[in] nextp; // 4. 将产品放入缓冲区 in (in 1) % n; // 5. 移动写指针循环缓冲区 /* --- 临界区结束 --- */ signal(mutex); // 6. 释放临界区锁V(mutex) signal(full); // 7. 告知消费者多了一个产品V(full) } while (TRUE); } // 消费者进程 void consumer() { do { wait(full); // 1. 申请一个产品P(full) wait(mutex); // 2. 申请进入临界区P(mutex) /* --- 临界区开始 --- */ nextc buffer[out]; // 3. 从缓冲区取出产品 out (out 1) % n; // 4. 移动读指针循环缓冲区 /* --- 临界区结束 --- */ signal(mutex); // 5. 释放临界区锁V(mutex) signal(empty); // 6. 告知生产者多了一个空位V(empty) consume the item in nextc; // 7. 消费产品 } while (TRUE); } // 主函数并发执行生产者和消费者 void main() { cobegin producer(); consumer(); coend } 方法二AND型信号量同时申请AND型信号量的核心思想是“原子性”。它要求进程要么同时获得所有需要的资源要么一个都别动避免死锁。在生产者-消费者问题中生产者需要同时申请空缓冲区和互斥锁。消费者需要同时申请满缓冲区和互斥锁。实现逻辑使用Swait操作代替wait。Swait(empty, mutex)表示同时申请空缓冲区和互斥锁。使用Ssignal操作代替signal。Ssignal(mutex, full)表示释放互斥锁并增加满缓冲区数量。伪代码实现// 信号量定义同上 semaphore mutex 1; semaphore empty n; semaphore full 0; // 生产者进程 void producer() { do { produce an item nextp; // 1. 生产一个产品 Swait(empty, mutex); // 2. 同时申请空缓冲区和互斥锁原子操作 /* --- 临界区开始 --- */ buffer[in] nextp; // 3. 将产品放入缓冲区 in (in 1) % n; // 4. 移动写指针 /* --- 临界区结束 --- */ Ssignal(mutex, full); // 5. 释放互斥锁并增加满缓冲区数量 } while (TRUE); } // 消费者进程 void consumer() { do { Swait(full, mutex); // 1. 同时申请满缓冲区和互斥锁原子操作 /* --- 临界区开始 --- */ nextc buffer[out]; // 2. 从缓冲区取出产品 out (out 1) % n; // 3. 移动读指针 /* --- 临界区结束 --- */ Ssignal(mutex, empty); // 4. 释放互斥锁并增加空缓冲区数量 consume the item in nextc; // 5. 消费产品 } while (TRUE); }️ 方法三管程Monitor管程是一种高级同步机制它将共享变量和操作这些变量的过程封装在一起保证同一时间只有一个进程能执行管程内的过程。实现逻辑定义一个管程ProducerConsumer包含缓冲区、指针、信号量和操作过程。生产者调用put()过程消费者调用get()过程。管程内部使用条件变量notfull和notempty来实现同步。伪代码实现// 管程定义 monitor ProducerConsumer { int in 0, out 0; item buffer[n]; condition notfull, notempty; // 条件变量 // 生产者调用的过程 void put(item x) { if ((in 1) % n out) { // 缓冲区满 notfull.wait(); // 生产者等待 } buffer[in] x; // 放入产品 in (in 1) % n; // 移动写指针 notempty.signal(); // 唤醒等待的消费者 } // 消费者调用的过程 void get(item x) { if (in out) { // 缓冲区空 notempty.wait(); // 消费者等待 } x buffer[out]; // 取出产品 out (out 1) % n; // 移动读指针 notfull.signal(); // 唤醒等待的生产者 } }; // 生产者进程 void producer() { item nextp; do { produce an item nextp; // 生产产品 ProducerConsumer.put(nextp); // 调用管程的put过程 } while (TRUE); } // 消费者进程 void consumer() { item nextc; do { ProducerConsumer.get(nextc); // 调用管程的get过程 consume the item in nextc; // 消费产品 } while (TRUE); } 高校期末真题精选题目 1在生产者-消费者问题中如果将wait(empty)和wait(mutex)的顺序颠倒会发生什么问题答案会导致死锁。假设缓冲区已满生产者先执行wait(mutex)获得了互斥锁然后执行wait(empty)时因为empty0而阻塞。此时消费者想进入临界区执行wait(mutex)但互斥锁已被生产者持有消费者也会阻塞。两者互相等待形成死锁。题目 2在管程实现中notfull.signal()和notempty.signal()的作用是什么答案notfull.signal()用于唤醒等待的生产者当缓冲区有空位时notempty.signal()用于唤醒等待的消费者当缓冲区有产品时。题目 32009年统考408真题三个进程P1、P2、P3互斥使用一个包含N个单元的缓冲区。P1每次生成一个正整数放入缓冲区。P2每次从缓冲区取出一个奇数并统计。P3每次从缓冲区取出一个偶数并统计。题目解析这道题的难点在于P2和P3不是随便取数的P2只要奇数P3只要偶数。这意味着我们需要把“产品”分类管理。信号量设置mutex 1互斥访问缓冲区。empty N空缓冲区数量。odd 0缓冲区中奇数的个数。even 0缓冲区中偶数的个数。伪代码答案semaphore mutex 1; semaphore empty N; semaphore odd 0; semaphore even 0; // 生产者 P1 void P1() { while(true) { int num produce(); // 生成正整数 wait(empty); // 申请空位 wait(mutex); // 申请互斥 put(num); // 放入缓冲区 signal(mutex); // 释放互斥 // 根据数值类型唤醒对应的消费者 if (num % 2 1) signal(odd); // 是奇数唤醒P2 else signal(even); // 是偶数唤醒P3 } } // 消费者 P2 (取奇数) void P2() { while(true) { wait(odd); // 申请奇数如果没有奇数则等待 wait(mutex); // 申请互斥 getodd(); // 取出奇数 signal(mutex); // 释放互斥 signal(empty); // 腾出空位唤醒P1 countodd(); // 统计 } } // 消费者 P3 (取偶数) void P3() { while(true) { wait(even); // 申请偶数 wait(mutex); // 申请互斥 geteven(); // 取出偶数 signal(mutex); // 释放互斥 signal(empty); // 腾出空位唤醒P1 counteven(); // 统计 } } 总结记录型信号量最基础适合理解同步和互斥的原理。AND型信号量强调原子性适合解决资源同时申请的问题。管程封装性好代码更清晰适合大型项目。掌握这三种方法不仅能帮你应对考试还能在面试中脱颖而出。赶紧动手实现一遍吧

更多文章