【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战

文章目录

  • 前言
  • [一、线程分离属性 pthread_attr_t](#一、线程分离属性 pthread_attr_t)
    • [1. 核心 API](#1. 核心 API)
    • [2. 使用流程](#2. 使用流程)
    • [3. 易错点](#3. 易错点)
  • [二、多进程 VS 多线程 全方位对比](#二、多进程 VS 多线程 全方位对比)
  • 三、线程间通信与临界区概念
    • [1. 线程通信方式](#1. 线程通信方式)
    • [2. 带来的问题:资源竞争](#2. 带来的问题:资源竞争)
    • [3. 核心概念](#3. 核心概念)
    • [4. 经典误区:一个加锁一个不加,锁有用吗?](#4. 经典误区:一个加锁一个不加,锁有用吗?)
  • [四、互斥锁 pthread_mutex](#四、互斥锁 pthread_mutex)
    • [1. 核心 API](#1. 核心 API)
    • [2. 标准使用模板](#2. 标准使用模板)
    • [3. 补充:非阻塞加锁](#3. 补充:非阻塞加锁)
  • 五、死锁
    • [1. 什么是死锁](#1. 什么是死锁)
    • [2. 死锁产生的四个必要条件](#2. 死锁产生的四个必要条件)
    • [3. 死锁规避方案](#3. 死锁规避方案)
  • [六、信号量 sem_t](#六、信号量 sem_t)
    • [1. 核心 API](#1. 核心 API)
    • [2. 信号量的两种经典用法](#2. 信号量的两种经典用法)
    • [3. 实战案例:三个线程按序打印 A、B、C](#3. 实战案例:三个线程按序打印 A、B、C)
  • 七、实战踩坑合集
    • [1. 输入函数坑:scanf ("% s") 缓冲区残留](#1. 输入函数坑:scanf ("% s") 缓冲区残留)
    • [2. 信号量初值坑](#2. 信号量初值坑)
    • [3. 主线程退出坑](#3. 主线程退出坑)
    • [4. 属性销毁坑](#4. 属性销毁坑)
  • 学习总结

前言

今天是系统编程学习的第七天,核心围绕多线程同步与通信展开,覆盖线程分离属性、多进程与多线程的优劣对比、互斥锁机制、死锁的产生与规避、信号量的同步 / 互斥用法四大模块。

学习过程中踩了不少经典面试坑:比如信号量初值设置错误导致线程顺序错乱、误以为互斥锁是硬件级变量保护、scanf 输入带空格字符串出现缓冲区残留等。本篇把知识点、API、实战案例、踩坑避坑一次性整理清楚,方便复习。


一、线程分离属性 pthread_attr_t

线程默认是可接合状态(PTHREAD_CREATE_JOINABLE) ,线程退出后系统不会自动回收资源,必须由主线程调用 pthread_join() 阻塞等待并回收;而**分离状态(PTHREAD_CREATE_DETACHED)**的线程,结束后系统自动回收资源,无需也不能调用 join。

1. 核心 API

函数 功能
pthread_attr_init 初始化线程属性结构体,必须先初始化再使用
pthread_attr_setdetachstate 设置线程分离状态
pthread_attr_destroy 销毁属性对象,释放内部资源
函数原型详解
复制代码
// 初始化属性,设置默认值
int pthread_attr_init(pthread_attr_t *attr);

// 设置分离状态
// detachstate 取值:
//   PTHREAD_CREATE_JOINABLE  默认,可接合
//   PTHREAD_CREATE_DETACHED  分离态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

// 销毁属性
int pthread_attr_destroy(pthread_attr_t *attr);

2. 使用流程

复制代码
pthread_attr_t attr;
pthread_attr_init(&attr);                    // 1.初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 2.设置分离

pthread_create(&tid, &attr, thread_func, NULL); // 3.创建线程,传入属性

pthread_attr_destroy(&attr);                 // 4.销毁属性(不影响已创建的线程)

3. 易错点

  1. 属性只是创建模板:线程创建完成后,属性就和线程无关了,此时 destroy 属性不会影响正在运行的线程。
  2. 分离态线程禁止调用 pthread_join,否则会出现未定义行为。
  3. 除了创建时设置,也可以在线程函数内部调用 pthread_detach(pthread_self()) 将自身设为分离态。

二、多进程 VS 多线程 全方位对比

从四个核心维度对比:

对比维度 多线程 多进程
运行效率 ✅ 更高。同一地址空间,线程切换开销小,调度快 ❌ 更低。进程切换需要切换页表、刷新缓存,开销大
安全性 ❌ 差。一个线程崩溃,整个进程退出,所有线程全部终止 ✅ 高。进程地址空间独立,单个进程崩溃不影响其他进程
通信实现 ✅ 简单。共享全局变量、堆内存,直接读写即可通信 ❌ 复杂。地址空间隔离,必须通过管道、共享内存、消息队列等 IPC 机制
编程复杂度 中等。共享资源会产生数据竞争,需要加锁保护 高。IPC 机制代码量大,调试难度高

适用场景总结

  • 多线程:计算密集型任务、频繁数据交互、并发量高的场景(如服务器、线程池)
  • 多进程:高可靠性要求、多任务隔离、崩溃不能影响整体的场景(如浏览器、守护进程)

三、线程间通信与临界区概念

1. 线程通信方式

同一进程的所有线程共享同一个虚拟地址空间,因此全局变量、静态变量、堆内存都是天然共享的,线程间可以直接通过这些共享变量通信。

2. 带来的问题:资源竞争

多个线程同时读写同一个共享变量时,会发生数据竞争 ,导致结果错乱。比如两个线程同时对全局变量执行 num++,最终结果会小于预期。

3. 核心概念

  • 原子操作:不能被 CPU 调度打断的最小执行单元,执行过程不会被切走。
  • 临界区(临界代码):被加锁和解锁包裹的代码片段,同一时刻最多只能有一个线程进入执行。

4. 经典误区:一个加锁一个不加,锁有用吗?

完全没用,互斥锁直接失效。 互斥锁本质是软件层面的约定 ,不是硬件级的变量保护。pthread_mutex_lock 只能拦住调用了这个函数的线程;如果有一个线程读写共享变量时不加锁,它会直接无视锁的存在修改内存,数据竞争依然存在。

铁律:所有访问同一共享资源的线程,必须使用同一把锁包裹临界区,只要漏一个,锁机制就作废。


四、互斥锁 pthread_mutex

互斥锁(Mutex)是解决多线程资源竞争最常用的工具,保证临界区代码的互斥执行。

1. 核心 API

函数 功能
pthread_mutex_init 初始化互斥锁,默认属性传 NULL
pthread_mutex_destroy 销毁互斥锁,释放资源
pthread_mutex_lock 加锁,拿不到锁则阻塞休眠
pthread_mutex_unlock 解锁,唤醒等待的线程
函数原型
复制代码
// 初始化锁,attr为NULL使用默认属性
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

// 销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

2. 标准使用模板

复制代码
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);

// 线程内部
pthread_mutex_lock(&lock);
// ===== 临界区:访问共享资源 =====
num = 100;
printf("%d\n", num);
// =================================
pthread_mutex_unlock(&lock);

3. 补充:非阻塞加锁

pthread_mutex_trylock():尝试加锁,拿不到锁不会阻塞,直接返回错误码,常用于避免死锁。


五、死锁

1. 什么是死锁

多个线程互相持有对方需要的锁,同时又等待对方释放锁,导致所有线程永久阻塞,程序卡死,就是死锁。

2. 死锁产生的四个必要条件

  1. 互斥条件:资源同一时间只能被一个线程占用
  2. 不可剥夺条件:已获得的锁不能被其他线程强行抢走
  3. 请求保持:线程持有已有锁的同时,又去请求新的锁
  4. 循环等待:线程之间形成环形的锁等待链

四个条件同时满足才会产生死锁,破坏任意一个即可避免死锁。

3. 死锁规避方案

  1. 所有线程按照相同的顺序加锁(破坏循环等待)
  2. 使用 pthread_mutex_trylock 尝试加锁,拿不到就释放已有锁(破坏请求保持)
  3. 避免锁嵌套,减少锁的持有时间

六、信号量 sem_t

信号量本质是一个内核维护的计数器 + 等待队列,支持两种原子操作:P 操作(申请资源,计数 - 1)、V 操作(释放资源,计数 + 1)。既可以实现互斥,也可以实现线程同步。

1. 核心 API

函数 功能
sem_init 初始化信号量
sem_destroy 销毁信号量
sem_wait P 操作:申请资源,计数 - 1,计数为 0 则阻塞
sem_post V 操作:释放资源,计数 + 1,唤醒等待线程
函数原型详解
复制代码
// 初始化信号量
// pshared: 0 = 本进程内线程间使用;非0 = 多进程间使用(需放在共享内存)
// value: 信号量初始计数值
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 销毁信号量
int sem_destroy(sem_t *sem);

// P操作:申请资源
int sem_wait(sem_t *sem);

// V操作:释放资源
int sem_post(sem_t *sem);

高频考点:sem_init 第二个参数 pshared

  • 0:用于同一进程的多个线程之间同步,信号量定义为全局变量即可
  • 写非 0:用于多个进程之间同步,信号量必须放在共享内存中

2. 信号量的两种经典用法

(1)二元信号量(初值 = 1):实现互斥

等价于互斥锁,sem_wait 对应加锁,sem_post 对应解锁。

(2)同步信号量(初值 = 0):控制执行顺序

用于保证线程的先后执行顺序,A 线程做完事再让 B 线程执行。

3. 实战案例:三个线程按序打印 A、B、C

需求

三个线程循环打印,严格按照 A → B → C 的顺序输出。

错误写法踩坑
  • 坑 1:sem_init 第二个参数错传 1,信号量初始化异常
  • 坑 2:信号量初值设置错误,比如第三个信号量初值设为 1,导致 C 线程抢先执行
正确实现
复制代码
// 信号量初值:sem_w=1, sem_r=0, sem_a=0
sem_init(&sem_w, 0, 1);
sem_init(&sem_r, 0, 0);
sem_init(&sem_a, 0, 0);
  • sem_w=1:A 线程初始拥有执行许可,最先运行
  • sem_r=0sem_a=0:B、C 线程初始无许可,阻塞等待前一个线程唤醒

执行时序:

  1. A 线程:sem_wait (sem_w) → 打印 A → sem_post (sem_r)
  2. B 线程被唤醒:sem_wait (sem_r) → 打印 B → sem_post (sem_a)
  3. C 线程被唤醒:sem_wait (sem_a) → 打印 C → sem_post (sem_w)
  4. 循环往复,严格保持 A→B→C 顺序

口诀:接力顺序打印,第一个线程信号量初值 = 1,后面全部初值 = 0。


七、实战踩坑合集

1. 输入函数坑:scanf ("% s") 缓冲区残留

问题 :密码验证题目中,输入 try again(带空格)时,scanf("%s") 遇到空格截断,只读取 try,剩下的 again 残留在输入缓冲区,程序退出后被 shell 读取,出现乱码输出。

解决 :题目要求按行输入时,使用 fgets() 读取整行,手动去掉换行符:

复制代码
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';

2. 信号量初值坑

同步场景下,后执行的线程对应的信号量初值必须为 0,否则线程会抢先执行,破坏顺序。

3. 主线程退出坑

主线程执行 return 结束进程时,所有子线程会被直接销毁,不管有没有执行完。所以需要用 pthread_join 或者死循环保证主线程不退出。

4. 属性销毁坑

pthread_attr_destroy 只是销毁属性对象,不会杀死已经创建好的线程。

学习总结

今天的核心是多线程同步,重点掌握:

  1. 线程分离属性的用法和两种状态的区别
  2. 多进程和多线程的优劣对比(面试必背)
  3. 互斥锁的作用、临界区概念,以及 "锁是约定不是硬件保护" 的本质
  4. 死锁的四个必要条件和规避方法
  5. 信号量的 P/V 操作,以及互斥、同步两种用法,重点掌握顺序打印的初值设置
相关推荐
Romantic_love_20 分钟前
USART串口初始化源码解析
stm32·单片机·嵌入式硬件·学习
小雪崩21 分钟前
嵌入式学习 day33:线程进阶
linux·c语言·学习
牢姐与蒯30 分钟前
Linux进程(四).进程优先级以及进程切换,进程调度
linux·运维·服务器·ubuntu
铁手飞鹰39 分钟前
VSCode Python .embed 嵌入式环境黄色波浪线问题
ide·vscode·python
大爱编程♡40 分钟前
Linux C 语言文件 IO 与构建工具实战指南
linux·服务器
LccKyI1 小时前
C#基础编程 面试题汇总(三)
笔记·c#
MartinYeung51 小时前
[论文学习]AI-Trader:实时金融市场中自主代理的基准测试
人工智能·深度学习·学习
懿路向前1 小时前
【HarmonyOS学习笔记】2026-08-27 | 端插件踩坑与设计取舍
笔记·学习·harmonyos
吴可可1231 小时前
VSCode助力机械臂轨迹开发
vscode