目录
[sem_post - V操作(发布/释放)](#sem_post - V操作(发布/释放))
1.死锁
- 概念
死锁是指在一组执行流中,每个执行流都占有一些资源,同时又在等待其他执行流释放其所占有的资源,从而导致所有执行流都无法继续执行的永久阻塞状态
- 死锁的四个必要条件
必要条件:如果发生了死锁,那么下面四个条件一定都满足。
非充分条件:但是,满足了下面四个条件,却不一定发生死锁
- 互斥条件 :一个资源一次只能被一个执行流使用;资源具有排他性,无法共享(资源独占)
- 请求与保持条件 :执行流在持有至少一个资源的同时,又请求新的资源;当请求的资源被其他执行流占用时,该进程会被阻塞,但不会释放已持有的资源(拿着碗里的,看着锅里的)
- 不剥夺条件 :执行流已获得的资源在未使用完毕前,不能被系统或其他执行流强制剥夺;只能由该执行流主动释放资源(不能强行抢走)
- 循环等待条件 :存在一个执行流资源的循环等待链;每个执行流都在等待下一个执行流所占用的资源(A等B,B等A(或A->B->C->A))
- 预防与处理死锁
理论上的预防方法(破坏必要条件):
- 破坏互斥条件:使资源可共享(但许多资源本质上是不可共享的,所以一般不可取)
- 破坏请求与保持条件:一次性分配资源策略或申请失败则释放所有资源(避免饥饿问题)
- 破坏不剥夺条件:允许系统强制剥夺资源
- 破坏循环等待条件:采用有序资源分配法
编程实践中的具体技巧:
- 加锁顺序一致:多个线程按相同顺序获取锁(这是有序资源分配的实现)
- 使用RAII机制:确保异常情况下锁能被释放
- 设置超时:避免无限期等待
系统层面的处理策略:
- 死锁检测与恢复:定期检查并采取措施恢复(执行流回退、终止等)
- 死锁避免算法:如银行家算法(运行时动态决策)(了解)
2.同步与竞态条件
- 同步是一种在保证数据安全的前提下,让执行流按照某种特定的顺序访问临界资源的机制
它不仅保证互斥(数据安全),还保证协作(如条件满足才执行) - 互斥是同步的一种特殊形式(只保证安全,不一定保证顺序),但同步包含互斥
- 合理的同步策略可以解决单纯互斥可能导致的资源分配不均、执行流饥饿等问题。反之会加剧死锁或饥饿风险
- 同步的基本思想是:执行流先竞争 访问临界资源的资格(竞争锁、信号量),然后再根据资源状态决定是立即访问还是排队等待
下面介绍两种实现同步机制的工具:条件变量与信号量
竞态条件是指多个执行流并发访问和操作共享资源时,由于执行顺序的不确定性,导致程序最终结果依赖于执行流相对时序的一种错误状态。其发生需要三个必要条件:存在共享资源、多个执行流并发访问、且至少一个执行流修改资源。竞态条件的根本原因是操作的非原子性------多个执行流的"读-改-写"等复合操作可能被交错执行,导致最终状态不符合预期,表现为数据不一致、程序崩溃或安全漏洞等问题
互斥管的是能不能同时做(权限);同步管的是什么时候能做(时机)
竞态条件VS死锁
| 特性 | 竞态条件 (Race Condition) | 死锁 (Deadlock) |
|---|---|---|
| 本质 | 执行顺序不确定导致结果错误 | 互相等待导致无法推进 |
| 是否阻塞 | 不一定阻塞,可能跑完了但结果是错的 | 一定阻塞,进程/线程卡死 |
| 必要条件 | 共享+并发+写 | 互斥+请求保持+不可剥夺+循环等待 |
| 修复手段 | 加锁、原子操作、不可变对象 | 破坏死锁的四个必要条件之一 |
关键区别 :竞态条件是**"乱"** (结果不可控),死锁是**"僵"**(进度卡死)
3.条件变量
条件变量 是一种执行流同步机制,它允许执行流在某个共享条件不满足时主动等待,直到其他执行流改变该条件并通知它(条件变量通常与互斥锁配合使用,本身不具备互斥能力)
3.1条件变量的核心思想
传统互斥的问题 :如果只用锁,执行流A拿不到锁只能死等(阻塞)或者自旋(空转CPU)。它不知道为什么 拿不到,也不知道什么时候 能拿到
条件变量引入了**"状态检查 + 主动休眠 + 通知唤醒"** 的三元组机制,必须与互斥锁配合使用
(1)执行流先检查状态(2)如果条件不满足:执行流原子地释放当前持有的锁 + 将自身挂起(睡眠)并注册一个"叫醒服务"(3)如果条件满足:执行流继续执行(4)当其他执行流改变了状态:它发出信号,唤醒那个正在睡觉的线程。
条件变量是一种"让执行流在条件不满足时智能睡眠、在条件满足时被精准叫醒"的机制,它通过"释放锁+睡眠"的原子操作,解决了传统互斥锁"盲等"导致的CPU浪费等问题
pthread_cond_t
| 特性 | 描述 |
|---|---|
| 头文件 | <pthread.h> |
| 类型定义 | typedef .... pthread_cond_t |
| 本质 | POSIX线程条件变量,用于线程间同步 |
| 配合机制 | 必须与互斥锁(pthread_mutex_t)配合使用 |
pthread_cond_init
| 项目 | 说明 |
|---|---|
| 函数原型 | int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); |
| 参数 | cond: 指向要初始化的条件变量指针 attr: 条件变量属性,通常置为NULL使用默认属性 |
| 返回值 | 成功返回0 失败返回错误码 |
| 核心功能 | 动态初始化条件变量 |
| 使用要点 | 1. 也可使用静态初始化宏:PTHREAD_COND_INITIALIZER 2. attr参数用于设置高级属性(如进程共享),通常不需要 3. 必须在条件变量使用前调用 |
// 静态初始化(编译时初始化)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
适用于全局或静态条件变量,不需要调用pthread_cond_destroy
pthread_cond_destroy
| 项目 | 说明 |
|---|---|
| 函数原型 | int pthread_cond_destroy(pthread_cond_t *cond); |
| 参数 | cond: 指向要销毁的条件变量指针 |
| 返回值 | 成功返回0 失败返回错误码 |
| 核心功能 | 释放条件变量占用的系统资源 |
| 使用要点 | 1**. 前提条件:没有执行流正在或即将等待此条件变量** 2. 销毁后不可再使用该条件变量 3. 对于静态初始化的条件变量,无需调用此函数 |
pthread_cond_wait
| 项目 | 说明 |
|---|---|
| 函数原型 | int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); |
| 参数 | cond: 要等待的条件变量 mutex: 调用****执行流 已持有的互斥锁 |
| 返回值 | 成功返回0 失败返回错误码 |
| 核心功能 | 使线程等待条件变量满足 |
| 使用要点 | 1. 原子性三步操作:释放互斥锁→执行流挂起→唤醒后重新加锁 2. 必须与互斥锁配合使用 3. 调用前执行流必须已持有mutex锁 4. 应使用while循环检查条件,防止虚假唤醒 |
调用执行流 必须已经持有mutex参数指向的互斥锁,函数内部会自动释放和重新获取该锁
pthread_cond_wait需要互斥量的核心原因:保证条件检查与线程挂起这两个操作的原子性如果不配合互斥锁,会出现以下竞态条件:执行流A检查条件发现不满足,在它准备挂起自己之前,执行流B可能已经修改了条件并发送了唤醒信号,这个信号就会被丢失。等到执行流A真正挂起后,它将永远等不到唤醒信号。互斥锁通过确保"检查条件"和"进入等待"这两个操作在锁的保护下作为一个原子操作执行,从而避免了这种信号丢失问题,同时也保证了共享条件状态在检查和修改时的一致性
// 标准等待模式:
pthread_mutex_lock(&mutex);
while (条件不满足) { // 必须用while,不能用if
pthread_cond_wait(&cond, &mutex);
}
// 执行操作...
pthread_mutex_unlock(&mutex);
// 给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
为了防止虚假唤醒导致程序逻辑错误,必须使用while循环而非if语句 。虚假唤醒是指执行流在没有收到显式唤醒信号的情况下从
pthread_cond_wait返回,可能由操作系统实现、信号中断或pthread_cond_broadcast唤醒所有线程等多种原因引起。使用while循环可以确保每次被唤醒后都重新检查条件是否真正满足:如果条件仍然不满足,线程会继续等待;只有条件确实满足时,线程才会退出循环执行后续操作。这种"重新验证"机制是条件变量正确性的关键保障
pthread_cond_signal
| 项目 | 说明 |
|---|---|
| 函数原型 | int pthread_cond_signal(pthread_cond_t *cond); |
| 参数 | cond: 要发送信号的条件变量 |
| 返回值 | 成功返回0 失败返回错误码 |
| 核心功能 | 唤醒至少一个等待该条件变量的线程 |
| 使用要点 | 1. 如果没有等待执行流,信号被忽略 2. 通常在修改条件后调用 3. 唤醒哪个执行流不确定(由系统调度决定) 4. 可在持有或不持有互斥锁的情况下调用 |
pthread_cond_signal:当只有一个等待线程能处理条件变化时使用
pthread_cond_broadcast:当多个等待线程都能处理,或不确定哪个应该处理时使用
pthread_cond_broadcast
| 项目 | 说明 |
|---|---|
| 函数原型 | int pthread_cond_broadcast(pthread_cond_t *cond); |
| 参数 | cond: 要广播信号的条件变量 |
| 返回值 | 成功返回0 失败返回错误码 |
| 核心功能 | 唤醒所有等待该条件变量的线程 |
| 使用要点 | 1. 执行流被唤醒后竞争互斥锁 2. 适用于多个执行流都能处理条件变化的情况 3. 性能开销通常大于signal 4. 常见使用场景:资源可用性变化影响多个执行流时 |
4.POSIX信号量
信号量的本质
计数器机制:信号量本质是一个非负整数计数器
间接资源判断:申请信号量时,通过计数器值间接判断资源是否就绪
资源管理分离:信号量只管理资源数量,具体访问哪个资源由程序员控制
与System V信号量的对比
| 特性 | POSIX信号量 | System V信号量 |
|---|---|---|
| 应用范围 | 主要用于线程间,也可用于进程间 | 主要用于进程间 |
| 接口风格 | 函数式接口(sem_xxx) |
系统调用式 |
| 性能 | 通常更轻量、更快 | 稍重,但功能更丰富 |
| 标准 | POSIX标准,更现代 | UNIX传统,广泛支持 |
| 持久性 | 无名信号量内存驻留,命名信号量文件系统持久 | 内核持久 |
- POSIX信号量核心函数
sem_t
| 特性 | 描述 |
|---|---|
| 头文件 | <semaphore.h> |
| 类型定义 | typedef .... sem_t |
| 本质 | POSIX计数信号量,用于线程/进程同步 |
| 核心特性 | 原子计数器,可用于资源管理和同步 |
sem_init-初始化信号量
| 项目 | 说明 |
|---|---|
| 函数原型 | int sem_init(sem_t *sem, int pshared, unsigned int value); |
| 头文件 | #include <semaphore.h> |
| 参数 | sem: 指向要初始化的信号量指针 pshared: 共享标志 value: 信号量初始值 |
| 参数详解 | pshared: • 0 - 线程间共享(同一进程内) • 非0 - 进程间共享(需要共享内存) value: 可用资源的初始数量 |
| 返回值 | 成功返回 0 失败返回 -1 并设置 errno |
| 核心功能 | 动态初始化一个无名信号量 |
| 使用注意 | 1. 无名信号量,生命周期与所在内存区域绑定 2. 进程间共享时,信号量必须位于共享内存区域 3. 对于线程间共享的简单场景,通常用互斥锁更合适 |
sem_destroy-销毁信号量
| 项目 | 说明 |
|---|---|
| 函数原型 | int sem_destroy(sem_t *sem); |
| 头文件 | #include <semaphore.h> |
| 参数 | sem: 指向要销毁的信号量指针 |
| 返回值 | 成功返回 0 失败返回 -1 并设置 errno |
| 核心功能 | 销毁已初始化的无名信号量,释放相关资源 |
| 使用注意 | 1. 确保没有线程/进程在等待该信号量 2. 销毁后不可再使用该信号量 3. 静态初始化的信号量无需销毁 4. 命名信号量使用 sem_close() 和 sem_unlink() 替代 |
sem_wait-P操作(等待/申请)
| 项目 | 说明 |
|---|---|
| 函数原型 | int sem_wait(sem_t *sem); |
| 头文件 | #include <semaphore.h> |
| 参数 | sem: 指向要等待的信号量指针 |
| 返回值 | 成功返回 0 失败返回 -1 并设置 errno |
| 核心功能 | 申请信号量,执行P操作(等待/减1) |
| 操作机制 | 1. 计数器值 > 0 : 立即返回,信号量值减1 2. 计数器值 = 0 : 线程阻塞等待,直到信号量值变为正数 |
| 原子性保证 | 整个"检查-减1"操作是原子的,无竞态条件 |
| 阻塞行为 | 可能被信号中断,此时返回 -1,errno 设为 EINTR |
sem_post - V操作(发布/释放)
| 项目 | 说明 |
|---|---|
| 函数原型 | int sem_post(sem_t *sem); |
| 头文件 | #include <semaphore.h> |
| 参数 | sem: 指向要释放的信号量指针 |
| 返回值 | 成功返回 0 失败返回 -1 并设置 errno |
| 核心功能 | 释放信号量,执行V操作(发布/加1) |
| 操作机制 | 1. 信号量值加1 2. 如果有线程在 sem_wait() 中阻塞,唤醒其中一个 |
| 原子性保证 | 整个"加1-唤醒"操作是原子的 |
| 唤醒策略 | 唤醒的线程由系统调度决定,不保证公平性 |
| 特殊情况 | 当信号量值达到 SEM_VALUE_MAX 时,sem_post 可能失败 |
与条件变量的区别
| 特性 | 信号量 | 条件变量 |
|---|---|---|
| 核心 | 计数器 | 等待队列+通知机制 |
| 独立性 | 可独立使用 | 必须配合互斥锁 |
| 状态 | 内置计数状态 | 程序员管理条件状态 |
| 通知 | sem_post自动唤醒 |
需显式调用signal/broadcast |
| 虚假唤醒 | 不存在 | 可能存在,需while循环 |