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. 将有限次循环改为死循环,理解两种模式的区别

相关推荐
姜鱼问生4 小时前
Nginx 缓存命中率监控:从 X-Cache-Status 到实时统计
运维·nginx·缓存
Apipi*4 小时前
30天速通Linux 第六章信号及信号处理
linux·运维·信号处理
天启HTTP5 小时前
爬虫防封核心原理:IP轮换机制与风控规避逻辑
linux·服务器·网络·爬虫·tcp/ip
运维行者_5 小时前
网络性能监控怎么做?从自动发现到根因分析的4个环节
运维·服务器·网络·人工智能·支持向量机
此时不提桶,更待何时6 小时前
06-14-A-Kafka集群运维与迁移实战详解
运维·kafka
hz567897 小时前
涉密视频会议设备配置指南:终端、音视频采集与配套设施选型
服务器·网络·数据库·安全·实时音视频·信息与通信·智能硬件
广州浮点FLOATLIC7 小时前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
优橙教育8 小时前
零基础学AI应用开发要多久?3个月能到什么水平
服务器·开发语言·网络·php
IT研究所8 小时前
AI-ITR平台如何减少客户问题反复升级?
大数据·运维·人工智能·低代码·自然语言处理·安全架构·企微
BLUcoding8 小时前
接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决
java·linux·springboot·aes·securerandom