Linux多线程编程完全指南:线程同步、互斥锁与生产者消费者模型

引言

在上一篇文章中,我们学习了线程的创建、退出和等待机制,并发现了多线程并发访问共享变量时的竞态条件问题。我们使用互斥锁解决了这个问题。今天,我们将在此基础上,深入探讨线程同步的经典问题------生产者消费者模型,并全面回顾进程与线程的区别、线程的实现方式等核心概念。


第一部分:上节回顾------信号量与互斥锁

一、信号量

信号量是用于进程/线程同步的机制,核心操作包括:

操作 函数 作用
初始化 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多线程编程系列的下篇,重点讲解了:

  1. 互斥锁的基本使用和注意事项

  2. 进程与线程的区别及Linux内核的实现方式

  3. 生产者消费者模型的同步逻辑和代码实现

  4. 先同步后互斥原则的重要性

面试高频考点:

  • 生产者消费者模型的代码实现和同步逻辑描述

  • 互斥锁与信号量的区别

  • 进程与线程的区别

  • 为什么需要先同步后互斥

学习建议:

  1. 理解生产者消费者模型后再看代码,不要死记硬背

  2. 动手运行代码,观察生产者和消费者的执行顺序

  3. 尝试修改生产者/消费者数量,观察效果

  4. 将有限次循环改为死循环,理解两种模式的区别

相关推荐
计算机安禾7 小时前
【Linux从入门到精通】第43篇:I/O调度算法与磁盘性能优化
linux·算法·性能优化
(Charon)7 小时前
【C++/Qt】Qt 实现 POP3/IMAP 邮件测试工具:连接邮箱服务器、登录与读取邮件
服务器·开发语言·c++
计算机安禾7 小时前
【Linux从入门到精通】第44篇:Linux网络协议栈与TCP参数调优
linux·网络协议·tcp/ip
rleS IONS7 小时前
Linux系统离线部署MySQL详细教程(带每步骤图文教程)
linux·mysql·adb
学不会pwn不改名7 小时前
【ArchLinux】如何制服国产免驱网卡
linux·运维·网络
一只小bit7 小时前
Docker 存储卷:本地文件与容器内部文件建立绑定关系
运维·docker·容器
可视化运维管理爱好者7 小时前
rg完整中文操作指南
linux·运维·服务器·ai
计算机安禾7 小时前
【Linux从入门到精通】第40篇:LAMP/LNMP环境一键部署脚本实战
android·linux·adb
‎ദ്ദിᵔ.˛.ᵔ₎7 小时前
Linux 基础指令
linux