引言
在上一篇文章中,我们学习了线程的创建、退出和等待机制,并发现了多线程并发访问共享变量时的竞态条件问题。我们使用互斥锁解决了这个问题。今天,我们将在此基础上,深入探讨线程同步的经典问题------生产者消费者模型,并全面回顾进程与线程的区别、线程的实现方式等核心概念。
第一部分:上节回顾------信号量与互斥锁
一、信号量
信号量是用于进程/线程同步的机制,核心操作包括:
| 操作 | 函数 | 作用 |
|---|---|---|
| 初始化 | sem_init() |
初始化信号量,设置初始值 |
| P操作 | sem_wait() |
信号量值减1,为0时阻塞 |
| V操作 | sem_post() |
信号量值加1,唤醒等待线程 |
| 销毁 | sem_destroy() |
销毁信号量 |
cpp
#include <semaphore.h>
// 初始化信号量
sem_t sem;
sem_init(&sem, 0, 1); // 第二个参数0表示线程间共享
// P操作(申请资源)
sem_wait(&sem);
// V操作(释放资源)
sem_post(&sem);
// 销毁
sem_destroy(&sem);
二、互斥锁(Mutex)
互斥锁是专门用于实现互斥访问的同步机制,与初值为1的信号量功能等价。
| 操作 | 函数 | 作用 |
|---|---|---|
| 初始化 | pthread_mutex_init |
初始化互斥锁 |
| 加锁 | pthread_mutex_lock |
锁被占用时阻塞 |
| 解锁 | pthread_mutex_unlock |
释放锁 |
| 销毁 | pthread_mutex_destroy |
销毁互斥锁 |
cpp
#include <pthread.h>
pthread_mutex_t mutex;
// 初始化
pthread_mutex_init(&mutex, NULL);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁
pthread_mutex_destroy(&mutex);
三、互斥锁与信号量的关系
| 特性 | 互斥锁 | 信号量(初值=1) |
|---|---|---|
| 本质 | 二进制锁 | 计数器 |
| 操作 | lock/unlock | P/V |
| 所有权 | 只有加锁线程能解锁 | 任何线程都可V操作 |
| 适用场景 | 保护临界区 | 资源计数、同步 |
第二部分:进程与线程的区别
一、基本概念
| 概念 | 定义 | 特点 |
|---|---|---|
| 进程 | 正在运行的程序,资源分配的基本单位 | 进程间相互隔离,独立的内存空间 |
| 线程 | 进程内部的执行路径,CPU调度的基本单位 | 同一进程的线程共享内存空间 |
核心区别:
-
不同进程之间的内存空间是隔离的,一个进程无法直接访问另一个进程的内存
-
同一进程内的多个线程共享内存空间,一个线程修改的变量,其他线程可以看到
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int shared_var = 0; // 全局变量,线程间共享
void* thread_func(void* arg) {
shared_var = 100;
printf("子线程: shared_var = %d\n", shared_var);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
printf("主线程: shared_var = %d\n", shared_var); // 输出100
return 0;
}
二、查看进程和线程ID
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("子线程: PID=%d, TID=%lu\n", getpid(), pthread_self());
return NULL;
}
int main() {
pthread_t tid;
printf("主线程: PID=%d, TID=%lu\n", getpid(), pthread_self());
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
return 0;
}
使用命令行查看线程:
# 查看进程及其线程
ps -eLf | grep program_name# 或使用 -T 选项
ps -T -p [PID]
三、线程的实现方式
| 实现方式 | 特点 | 优缺点 |
|---|---|---|
| 用户级线程 | 用户空间管理,内核只看到一条执行路径 | 创建快,但无法利用多核 |
| 内核级线程 | 内核直接管理,内核可见多条路径 | 可利用多核,Linux采用此方式 |
| 组合模型 | 用户级和内核级结合 | 兼顾灵活性和性能 |
Linux内核的独特视角:
-
Linux内核没有单独的线程概念
-
线程被视作与其他进程共享资源的进程
-
每个线程都有独立的
task_struct(进程描述符) -
通过共享内存空间、文件描述符等实现线程特性
第三部分:生产者消费者模型
一、问题描述
生产者消费者模型(Producer-Consumer Problem)是操作系统的经典同步问题:
-
生产者:向缓冲区中写入数据
-
消费者:从缓冲区中读取数据
-
缓冲区:有限大小的共享区域

二、同步条件
| 条件 | 说明 | 控制方式 |
|---|---|---|
| 互斥访问 | 同一时刻只能一个线程操作缓冲区 | 互斥锁 |
| 缓冲区非满 | 满时生产者不能写入 | 信号量 empty |
| 缓冲区非空 | 空时消费者不能读取 | 信号量 full |
三、同步逻辑设计
重要原则:先同步,后互斥
即先执行P操作(同步判断),再加锁(互斥访问),避免死锁。
四、代码实现
头文件与定义
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
#define BUFFER_SIZE 30 // 缓冲区大小
#define PRODUCER_NUM 2 // 生产者数量
#define CONSUMER_NUM 3 // 消费者数量
#define PRODUCE_COUNT 30 // 每个生产者生产数量
#define CONSUME_COUNT 20 // 每个消费者消费数量
int buffer[BUFFER_SIZE]; // 缓冲区
int in = 0; // 生产者写入位置
int out = 0; // 消费者读取位置
sem_t empty; // 空闲格子数(生产者用)
sem_t full; // 满格子数(消费者用)
pthread_mutex_t mutex; // 互斥锁
初始化
cpp
void init() {
// 初始化信号量
sem_init(&empty, 0, BUFFER_SIZE); // 初始有BUFFER_SIZE个空闲
sem_init(&full, 0, 0); // 初始没有数据
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 随机数种子
srand(time(NULL));
}
生产者函数
cpp
void* producer(void* arg) {
for (int i = 0; i < PRODUCE_COUNT; i++) {
// 1. 等待空闲格子
sem_wait(&empty);
// 2. 加锁
pthread_mutex_lock(&mutex);
// 3. 生产数据
int data = rand() % 100;
buffer[in] = data;
printf("生产者[%lu] 写入位置[%d]: %d\n",
pthread_self(), in, data);
// 4. 更新写入位置
in = (in + 1) % BUFFER_SIZE;
// 5. 解锁
pthread_mutex_unlock(&mutex);
// 6. 通知消费者
sem_post(&full);
// 模拟生产耗时
usleep(rand() % 100000);
}
return NULL;
}
消费者函数
cpp
void* consumer(void* arg) {
for (int i = 0; i < CONSUME_COUNT; i++) {
// 1. 等待有数据
sem_wait(&full);
// 2. 加锁
pthread_mutex_lock(&mutex);
// 3. 消费数据
int data = buffer[out];
printf("消费者[%lu] 读取位置[%d]: %d\n",
pthread_self(), out, data);
// 4. 更新读取位置
out = (out + 1) % BUFFER_SIZE;
// 5. 解锁
pthread_mutex_unlock(&mutex);
// 6. 通知生产者有空闲格子
sem_post(&empty);
// 模拟消费耗时
usleep(rand() % 100000);
}
return NULL;
}
主函数
cpp
int main() {
pthread_t producers[PRODUCER_NUM];
pthread_t consumers[CONSUMER_NUM];
init();
// 创建生产者线程
for (int i = 0; i < PRODUCER_NUM; i++) {
pthread_create(&producers[i], NULL, producer, NULL);
}
// 创建消费者线程
for (int i = 0; i < CONSUMER_NUM; i++) {
pthread_create(&consumers[i], NULL, consumer, NULL);
}
// 等待生产者结束
for (int i = 0; i < PRODUCER_NUM; i++) {
pthread_join(producers[i], NULL);
}
// 等待消费者结束
for (int i = 0; i < CONSUMER_NUM; i++) {
pthread_join(consumers[i], NULL);
}
// 销毁资源
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
五、为什么先同步后互斥?
cpp
// ❌ 错误顺序:先加锁,后同步
pthread_mutex_lock(&mutex);
sem_wait(&empty); // 如果缓冲区满,这里会阻塞
// 此时锁仍然被持有,消费者无法进入
// 如果消费者也无法进入,形成死锁!
// ✅ 正确顺序:先同步,后加锁
sem_wait(&empty); // 先判断是否能操作
pthread_mutex_lock(&mutex); // 再独占缓冲区
// 操作...
pthread_mutex_unlock(&mutex);
sem_post(&full);
第四部分:死循环与有限次循环
一、两种模式对比
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 有限次循环 | 生产/消费固定数量后结束 | 批量处理任务 |
| 死循环 | 持续生产/消费,永不停止 | 服务器、守护进程 |
cpp
// 有限次循环
for (int i = 0; i < PRODUCE_COUNT; i++) {
// 生产数据
}
// 死循环
while (1) {
// 生产数据
}
二、死循环版本的特点
-
生产者持续生产,消费者持续消费
-
程序永远不会主动退出(需Ctrl+C或kill终止)
-
常用于服务端程序、消息队列等场景
总结
一、互斥锁与信号量总结
| 特性 | 互斥锁 | 信号量 |
|---|---|---|
| 初始化 | pthread_mutex_init |
sem_init |
| 加锁/P | pthread_mutex_lock |
sem_wait |
| 解锁/V | pthread_mutex_unlock |
sem_post |
| 有所有权 | ✅ 只有加锁才能解锁 | ❌ 任何线程都可V |
| 适用 | 互斥访问 | 资源计数、同步 |
二、生产者消费者模型核心要点
| 要点 | 说明 |
|---|---|
| 同步条件 | 缓冲区非满(生产者)、非空(消费者) |
| 互斥条件 | 同一时刻只有一个线程操作缓冲区 |
| 信号量empty | 初始为缓冲区大小,控制生产者 |
| 信号量full | 初始为0,控制消费者 |
| 操作顺序 | 先同步(P操作),后互斥(加锁) |
三、进程与线程总结
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立空间 | 共享空间 |
| 通信方式 | IPC(管道、共享内存等) | 直接访问共享变量 |
| 创建开销 | 大 | 小 |
| 切换开销 | 大 | 小 |
| Linux中的本质 | task_struct | task_struct(共享资源) |
本文是Linux多线程编程系列的下篇,重点讲解了:
-
互斥锁的基本使用和注意事项
-
进程与线程的区别及Linux内核的实现方式
-
生产者消费者模型的同步逻辑和代码实现
-
先同步后互斥原则的重要性
面试高频考点:
-
生产者消费者模型的代码实现和同步逻辑描述
-
互斥锁与信号量的区别
-
进程与线程的区别
-
为什么需要先同步后互斥
学习建议:
-
理解生产者消费者模型后再看代码,不要死记硬背
-
动手运行代码,观察生产者和消费者的执行顺序
-
尝试修改生产者/消费者数量,观察效果
-
将有限次循环改为死循环,理解两种模式的区别