【操作系统实验】Linux 下多线程同步与互斥实战——生产者 - 消费者模型
摘要:
本文记录了在 Ubuntu Linux 环境下,使用 GCC 和 POSIX 线程库(pthread)解决经典并发编程问题——“生产者 - 消费者问题”的全过程。文章详细分析了信号量(Semaphore)与互斥锁(Mutex)的配合机制,并提供了完整的可运行代码及编译调试步骤。对于正在学习操作系统课程的同学,这是一份实用的参考指南。
一、前言
最近在进行操作系统的课程设计,核心任务是理解进程 / 线程间的同步与互斥机制。经典的“生产者 - 消费者问题”是理解这一概念的基石。
我的开发环境是在 Windows 宿主机上通过 VMware Workstation 运行的 Ubuntu 64 位 虚拟机。这种环境既能保证系统的安全性,又能提供原汁原味的 Linux 命令行体验,非常适合进行底层系统编程的学习。
二、问题分析
在这个实验中,我们需要模拟多个生产者和多个消费者共享一个固定大小的缓冲区:
- 生产者(Producer):负责生产数据并放入缓冲区。如果缓冲区满了,必须等待。
- 消费者(Consumer):负责从缓冲区取出数据。如果缓冲区空了,必须等待。
- 互斥访问:无论生产还是消费,同一时刻只能有一个线程操作缓冲区,以防止数据错乱。
为了实现上述逻辑,我们需要三个关键的同步工具:
mutex(互斥锁):保护缓冲区的互斥访问。empty(信号量):记录空闲缓冲区的数量,初始值为缓冲区总大小 $M$。full(信号量):记录已填充数据的缓冲区数量,初始值为 $0$。
三、核心代码实现
以下是我在 Ubuntu 终端中编写并验证通过的完整代码(producer_consumer.c)。代码使用了 <semaphore.h> 和 <pthread.h> 库。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 5 // 缓冲区大小 M
#define PRODUCER_NUM 2 // 生产者数量
#define CONSUMER_NUM 2 // 消费者数量
int buffer[BUFFER_SIZE];
int in = 0; // 生产者写入位置
int out = 0; // 消费者读取位置
// 定义信号量和互斥锁
sem_t empty_sem;
sem_t full_sem;
pthread_mutex_t mutex;
// 生产者函数
void *producer(void *arg) {
int id = *(int *)arg;
while (1) {
sleep(1); // 模拟生产耗时
sem_wait(&empty_sem); // P(empty): 申请空位
pthread_mutex_lock(&mutex); // Lock: 进入临界区
// --- 写入数据 ---
buffer[in] = 1;
printf("[生产者 %d] 放入数据到位置 %d\n", id, in);
in = (in + 1) % BUFFER_SIZE;
pthread_mutex_unlock(&mutex); // Unlock: 离开临界区
sem_post(&full_sem); // V(full): 增加满位计数
}
return NULL;
}
// 消费者函数
void *consumer(void *arg) {
int id = *(int *)arg;
while (1) {
sleep(2); // 模拟消费耗时
sem_wait(&full_sem); // P(full): 申请数据
pthread_mutex_lock(&mutex); // Lock: 进入临界区
// --- 读取数据 ---
printf("[消费者 %d] 从位置 %d 取出数据\n", id, out);
buffer[out] = 0;
out = (out + 1) % BUFFER_SIZE;
pthread_mutex_unlock(&mutex); // Unlock: 离开临界区
sem_post(&empty_sem); // V(empty): 增加空位计数
}
return NULL;
}
int main() {
pthread_t prod_threads[PRODUCER_NUM], cons_threads[CONSUMER_NUM];
int prod_ids[PRODUCER_NUM], cons_ids[CONSUMER_NUM];
// 初始化
sem_init(&empty_sem, 0, BUFFER_SIZE);
sem_init(&full_sem, 0, 0);
pthread_mutex_init(&mutex, NULL);
// 创建线程
for (int i = 0; i < PRODUCER_NUM; i++) {
prod_ids[i] = i;
pthread_create(&prod_threads[i], NULL, producer, &prod_ids[i]);
}
for (int i = 0; i < CONSUMER_NUM; i++) {
cons_ids[i] = i;
pthread_create(&cons_threads[i], NULL, consumer, &cons_ids[i]);
}
// 等待线程结束(此处为死循环演示,实际需配合退出逻辑)
for (int i = 0; i < PRODUCER_NUM; i++) pthread_join(prod_threads[i], NULL);
for (int i = 0; i < CONSUMER_NUM; i++) pthread_join(cons_threads[i], NULL);
// 销毁资源
sem_destroy(&empty_sem);
sem_destroy(&full_sem);
pthread_mutex_destroy(&mutex);
return 0;
}
四、编译与运行指南
在 Ubuntu 虚拟机中,请按照以下步骤操作:
-
保存文件:将上述代码保存为
producer_consumer.c。 -
编译命令:注意需要链接
pthread库。gcc producer_consumer.c -o pc_demo -lpthread -
运行程序:
./pc_demo -
观察输出:你会看到生产者和消费者交替打印日志,且不会出现缓冲区溢出或读取空数据的情况。按
Ctrl+C终止程序。
五、实验心得与注意事项
- P/V 操作顺序至关重要:在生产者中,必须先
wait(empty)再lock(mutex);在消费者中,必须先wait(full)再lock(mutex)。如果顺序反了(先锁后等信号量),极易导致死锁。 - 编译报错处理:如果在 Ubuntu 上编译提示找不到
semaphore.h,请确保安装了libc6-dev包(通常默认已安装)。 - 虚拟机的优势:如果在实验中不小心写错了代码导致系统卡死,直接关闭 VMware 虚拟机重启即可,完全不会影响宿主机的 Windows 系统,这大大降低了试错成本。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)