当前位置: 首页 > news >正文

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

引言

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


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

一、信号量

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

操作函数作用
初始化sem_init()初始化信号量,设置初始值
P操作sem_wait()信号量值减1,为0时阻塞
V操作sem_post()信号量值加1,唤醒等待线程
销毁sem_destroy()销毁信号量
#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销毁互斥锁
#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/unlockP/V
所有权只有加锁线程能解锁任何线程都可V操作
适用场景保护临界区资源计数、同步

第二部分:进程与线程的区别

一、基本概念

概念定义特点
进程正在运行的程序,资源分配的基本单位进程间相互隔离,独立的内存空间
线程进程内部的执行路径,CPU调度的基本单位同一进程的线程共享内存空间

核心区别:

  • 不同进程之间的内存空间是隔离的,一个进程无法直接访问另一个进程的内存

  • 同一进程内的多个线程共享内存空间,一个线程修改的变量,其他线程可以看到

#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

#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操作(同步判断),再加锁(互斥访问),避免死锁。

四、代码实现

头文件与定义
#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; // 互斥锁
初始化
void init() { // 初始化信号量 sem_init(&empty, 0, BUFFER_SIZE); // 初始有BUFFER_SIZE个空闲 sem_init(&full, 0, 0); // 初始没有数据 // 初始化互斥锁 pthread_mutex_init(&mutex, NULL); // 随机数种子 srand(time(NULL)); }
生产者函数
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; }
消费者函数
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; }
主函数
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; }

五、为什么先同步后互斥?

// ❌ 错误顺序:先加锁,后同步 pthread_mutex_lock(&mutex); sem_wait(&empty); // 如果缓冲区满,这里会阻塞 // 此时锁仍然被持有,消费者无法进入 // 如果消费者也无法进入,形成死锁! // ✅ 正确顺序:先同步,后加锁 sem_wait(&empty); // 先判断是否能操作 pthread_mutex_lock(&mutex); // 再独占缓冲区 // 操作... pthread_mutex_unlock(&mutex); sem_post(&full);

第四部分:死循环与有限次循环

一、两种模式对比

模式特点适用场景
有限次循环生产/消费固定数量后结束批量处理任务
死循环持续生产/消费,永不停止服务器、守护进程
// 有限次循环 for (int i = 0; i < PRODUCE_COUNT; i++) { // 生产数据 } // 死循环 while (1) { // 生产数据 }

二、死循环版本的特点

  • 生产者持续生产,消费者持续消费

  • 程序永远不会主动退出(需Ctrl+C或kill终止)

  • 常用于服务端程序、消息队列等场景


总结

一、互斥锁与信号量总结

特性互斥锁信号量
初始化pthread_mutex_initsem_init
加锁/Ppthread_mutex_locksem_wait
解锁/Vpthread_mutex_unlocksem_post
有所有权✅ 只有加锁才能解锁❌ 任何线程都可V
适用互斥访问资源计数、同步

二、生产者消费者模型核心要点

要点说明
同步条件缓冲区非满(生产者)、非空(消费者)
互斥条件同一时刻只有一个线程操作缓冲区
信号量empty初始为缓冲区大小,控制生产者
信号量full初始为0,控制消费者
操作顺序先同步(P操作),后互斥(加锁)

三、进程与线程总结

维度进程线程
资源分配独立空间共享空间
通信方式IPC(管道、共享内存等)直接访问共享变量
创建开销
切换开销
Linux中的本质task_structtask_struct(共享资源)

本文是Linux多线程编程系列的下篇,重点讲解了:

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

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

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

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

面试高频考点:

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

  • 互斥锁与信号量的区别

  • 进程与线程的区别

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

学习建议:

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

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

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

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

http://www.jsqmd.com/news/739824/

相关文章:

  • 3步完成Amlogic电视盒子Armbian系统安装:从闲置硬件到高效服务器
  • 如何彻底告别网盘限速:LinkSwift八大网盘直链下载助手终极指南
  • TrendForge 每日精选 9 个热门开源项目,mattpocock/skills 新增 3645 星成“今日之星”
  • 机器人通用化训练:世界基础模型与合成数据技术突破
  • 最短路径-Dijkstra算法(迪杰斯特拉算法)
  • 向量搜索技术解析:从原理到工程实践
  • FPGA在智能电网中的实时处理与可靠性设计
  • 2026天津专业防水公司TOP5推荐:卫生间、外墙、楼顶、地下室渗漏专业公司推荐(2026年5月天津最新深度调研方案) - 防水百科
  • 如何使用face-api.js快速实现人脸识别:7个实用技巧与解决方案
  • 别再死记硬背了!用ENSP模拟器一步步拆解华为MSTP、VRRP、DHCP中继的联动原理与配置
  • 手把手教你用libexpat解析XML配置文件:一个C语言嵌入式项目的完整实战
  • 告别双系统折腾:用VMware+Ubuntu+Miniconda打造你的轻量级PyTorch学习环境
  • 异步强化学习框架优化LLM训练效率
  • 基于Whisper的音频转录实战:从架构设计到生产部署
  • 2026年3月靠谱的日本留学就业品牌推荐,EJU培训/日本留学签证办理/日语培训,日本留学就业中心推荐口碑分析 - 品牌推荐师
  • AI智能体如何成为基础设施炼金术士:从IaC到生产就绪的自动化实践
  • 高通SM6225 GKI 2.0编译效率提升指南:巧用SKIP_MRPROPER与模块化编译
  • OrgChart.js终极指南:5分钟快速创建专业组织结构图
  • 内容创作团队如何借助 Taotoken 调用不同模型优化生成流程
  • Nacos数据迁移实战:从MySQL平滑切换到国产达梦数据库(附完整SQL与避坑点)
  • 物联网固件加密性能瓶颈诊断手册:从函数调用开销、内存对齐、分支预测失败到SIMD指令未使能——一份可立即执行的12步自检清单
  • HFSS新手避坑指南:从零开始手把手教你仿真半波对称阵子天线(附完整模型文件)
  • 如何用Vin象棋快速提升棋艺:免费AI辅助工具完全指南
  • 高效使用喜马拉雅音频下载工具:专业操作指南与实用技巧
  • AX88U梅林固件实战:用一条命令搞定Switch联网屏蔽,告别BAN机焦虑
  • 从Git命令到可视化图表:手把手教你用Mermaid gitGraph复盘复杂合并冲突
  • Open UI5 源代码解析之1143:ValueHelpField.js
  • 从零到一:手把手教你用ArcGIS和SWAT-CUP搞定流域面源污染模拟(附数据与代码)
  • 告别手动拖拽!用FGUI+Unity 2022 LTS实现UI资源自动化发布与热更新
  • 从扫地机器人到AGV:5种常见移动机器人底盘,哪种更适合你的项目?(附ROS适配建议)