文章目录
- 前言
- 一、线程分离属性 pthread_attr_t
- 1. 核心 API
- 2. 使用流程
- 3. 易错点
- 二、多进程 VS 多线程 全方位对比
- 适用场景总结
- 三、线程间通信与临界区概念
- 1. 线程通信方式
- 2. 带来的问题:资源竞争
- 3. 核心概念
- 4. 经典误区:一个加锁一个不加,锁有用吗?
- 四、互斥锁 pthread_mutex
- 1. 核心 API
- 2. 标准使用模板
- 3. 补充:非阻塞加锁
- 五、死锁
- 1. 什么是死锁
- 2. 死锁产生的四个必要条件
- 3. 死锁规避方案
- 六、信号量 sem_t
- 1. 核心 API
- 2. 信号量的两种经典用法
- 3. 实战案例:三个线程按序打印 A、B、C
- 七、实战踩坑合集
- 1. 输入函数坑:scanf ("% s") 缓冲区残留
- 2. 信号量初值坑
- 3. 主线程退出坑
- 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. 易错点
二、多进程 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. 死锁产生的四个必要条件
四个条件同时满足才会产生死锁,破坏任意一个即可避免死锁。
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=0、sem_a=0: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 只是销毁属性对象,不会杀死已经创建好的线程。
学习总结
今天的核心是多线程同步,重点掌握:

