Linux软件编程:线程间通信
目录
一、线程间通信基础
1. 概念
2. 通信基础:共享空间
二、互斥锁(Mutex)
1. 概念
2. 使用流程
3. 函数接口
三、死锁
1. 概念
2. 死锁产生的 4 个必要条件
3. 避免死锁的方法
四、信号量(Semaphore)
1. 概念
2. 函数接口
3. 应用场景
一、线程间通信基础
1. 概念
线程间通信指多个线程之间传递信息的过程,是多线程协作完成任务的核心机制
2. 通信基础:共享空间
同一进程中的多个线程共享以下资源,这是线程间通信的基础:
- 文本段(代码区)
- 数据段(全局变量、静态变量等)
- 堆区(动态分配的内存空间)
注意:线程独享栈区(默认 8M),栈区数据不共享;因此线程间通信主要依赖共享的全局变量、堆区数据等
采用全局变量的原因:
- 进程是操作系统资源分配的最小单元
- 每个进程空间独立的,包含文本段+数据段(全局变量)+系统数据段
- 一个进程中的多个线程独享栈空间,文本段、数据段、堆区进程多线程共享
同一进程的线程共享全局变量、静态变量、堆区数据(malloc 分配),但局部变量(栈区)不共享;因此线程间通信需通过共享变量,避免使用栈区数据
- 多线程同时操作共享空间会引发资源竞争,需要加上互斥锁解决资源竞争问题
二、互斥锁(Mutex)
1. 概念
- 互斥锁是解决多线程资源竞争的核心机制,可视为一种 "独占性资源"
- 特性:同一时间只能有一个线程获得锁,加锁期间其他线程需等待解锁后才能再次加锁
- 临界区:加锁和解锁之间的代码段,即被互斥锁保护的共享资源操作代码
- 只能防止多个线程对资源的竞争,不能决定代码的先后执行顺序
- 原子操作:CPU 执行加锁 / 解锁操作时无法切换任务,保证操作的不可分割性
2. 使用流程
1. 定义互斥锁(全局变量)
2. 对锁初始化
3. 操作全局资源前先加锁
4. 如果加锁成功则完成对全局资源操作
5. 如果加锁失败则表示有人占用资源,必须等待其余人释放锁资源才能加锁成功
6. 直到加锁成功使用该全局资源
3. 函数接口
函数名 | 原型 | 功能 | 参数说明 | 返回值 |
---|---|---|---|---|
pthread_mutex_init | int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) | 初始化互斥锁 | - 互斥锁变量地址 锁属性(默认 NULL,使用默认属性) | 成功返回 0 失败返回 - 1 |
pthread_mutex_lock | int pthread_mutex_lock(pthread_mutex_t *mutex) | 互斥锁加锁 |
互斥锁变量地址 | 成功返回 0 失败返回 - 1(若锁已被占用,会阻塞等待) |
pthread_mutex_unlock | int pthread_mutex_unlock(pthread_mutex_t *mutex) | 互斥锁解锁 |
互斥锁变量地址 | 成功返回 0 失败返回 - 1 |
pthread_mutex_destroy | int pthread_mutex_destroy(pthread_mutex_t *mutex) | 销毁互斥锁 |
互斥锁变量地址 | 成功返回 0 失败返回 - 1 |
三、死锁
1. 概念
多线程因加锁 / 解锁顺序错误,导致线程相互等待对方释放锁资源,程序无法继续执行的状态。
2. 死锁产生的 4 个必要条件
条件 | 说明 |
---|---|
互斥条件 | 资源(如锁)只能被一个线程占用 |
不可剥夺条件 | 线程占用的资源不能被强制剥夺 |
请求保持条件 | 线程持有部分资源,同时请求其他资源 |
循环等待条件 | 线程间形成资源请求循环(如线程 1 等待线程 2 的锁,线程 2 等待线程 1 的锁) |
3. 避免死锁的方法
- 保持加锁顺序一致:所有线程按固定顺序加锁(如先锁 A 再锁 B)
- 使用非阻塞加锁:用
pthread_mutex_trylock
(尝试加锁,失败时不阻塞,返回错误码)替代pthread_mutex_lock
- 限时加锁:设置加锁超时时间,超时后释放已持有的锁
四、信号量(Semaphore)
1. 概念
- 信号量是一种资源
- 信号量只能完成四种操作:初始化、销毁、申请、释放
- 如果信号量资源数为0,申请资源会阻塞等待,直到占用资源的任务释放资源,资源数不为0时才能申请到资源并继续向下执行
- 释放资源不会阻塞
2. 函数接口
函数名 | 原型 | 功能 | 参数说明 | 返回值 |
---|---|---|---|---|
sem_init | int sem_init(sem_t *sem, int pshared, unsigned int value) | 初始化信号量 | - 信号量变量地址 0 表示线程间共享,非 0 表示进程间共享 初始资源数量 | 成功返回 0 失败返回 - 1 |
sem_destroy | int sem_destroy(sem_t *sem) | 销毁信号量 |
信号量变量地址 | 成功返回 0 失败返回 - 1 |
sem_wait | int sem_wait(sem_t *sem) | 申请信号量(P 操作) |
信号量变量地址 | 成功返回 0(资源数 - 1) 失败返回 - 1;资源数为 0 时阻塞 |
sem_post | int sem_post(sem_t *sem) | 释放信号量(V 操作) |
信号量变量地址 | 成功返回 0(资源数 + 1) 失败返回 - 1 |
注意:
- 申请信号量会让信号量资源数-1
- 如果信号量资源数为0,则会阻塞等待,直到有任务释放资源,才能拿到资源并继续向下 执行
3. 应用场景
- 控制并发访问数量(如限制同时访问共享内存的线程数)
- 实现线程间的同步(如生产者 - 消费者模型中控制数据读写顺序)
注意:
- 互斥锁与信号量的区别
- 互斥锁:用于 "独占" 资源(资源数固定为 1),解决资源竞争
- 信号量:用于 "计数" 资源(资源数可自定义),控制并发数量或同步
2.临界区设计原则
- 临界区代码应尽可能精简,仅包含对共享资源的操作,减少线程等待时间,提高效率