Linux 之 【多线程】(死锁、同步与竞态条件、条件变量、pthread_cond_xxx、POSIX信号量、sem_xxx)

目录

1.死锁

2.同步与竞态条件

竞态条件VS死锁

3.条件变量

3.1条件变量的核心思想

pthread_cond_t

pthread_cond_init

pthread_cond_destroy

pthread_cond_wait

pthread_cond_signal

pthread_cond_broadcast

4.POSIX信号量

sem_t

sem_init-初始化信号量

sem_destroy-销毁信号量

sem_wait-P操作(等待/申请)

[sem_post - V操作(发布/释放)](#sem_post - V操作(发布/释放))

与条件变量的区别


1.死锁

  • 概念

死锁是指在一组执行流中,每个执行流都占有一些资源,同时又在等待其他执行流释放其所占有的资源,从而导致所有执行流都无法继续执行的永久阻塞状态

  • 死锁的四个必要条件

必要条件:如果发生了死锁,那么下面四个条件一定都满足。

非充分条件:但是,满足了下面四个条件,却不一定发生死锁

  1. 互斥条件 :一个资源一次只能被一个执行流使用;资源具有排他性,无法共享(资源独占)
  2. 请求与保持条件 :执行流在持有至少一个资源的同时,又请求新的资源;当请求的资源被其他执行流占用时,该进程会被阻塞,但不会释放已持有的资源(拿着碗里的,看着锅里的)
  3. 不剥夺条件 :执行流已获得的资源在未使用完毕前,不能被系统或其他执行流强制剥夺;只能由该执行流主动释放资源(不能强行抢走)
  4. 循环等待条件 :存在一个执行流资源的循环等待链;每个执行流都在等待下一个执行流所占用的资源(A等B,B等A(或A->B->C->A))
  • 预防与处理死锁

理论上的预防方法(破坏必要条件):

  1. 破坏互斥条件:使资源可共享(但许多资源本质上是不可共享的,所以一般不可取)
  2. 破坏请求与保持条件:一次性分配资源策略或申请失败则释放所有资源(避免饥饿问题)
  3. 破坏不剥夺条件:允许系统强制剥夺资源
  4. 破坏循环等待条件:采用有序资源分配法

编程实践中的具体技巧:

  1. 加锁顺序一致:多个线程按相同顺序获取锁(这是有序资源分配的实现)
  2. 使用RAII机制:确保异常情况下锁能被释放
  3. 设置超时:避免无限期等待

系统层面的处理策略:

  1. 死锁检测与恢复:定期检查并采取措施恢复(执行流回退、终止等)
  2. 死锁避免算法:如银行家算法(运行时动态决策)(了解)

2.同步与竞态条件

  1. 同步是一种在保证数据安全的前提下,让执行流按照某种特定的顺序访问临界资源的机制
    它不仅保证互斥(数据安全),还保证协作(如条件满足才执行)
  2. 互斥是同步的一种特殊形式(只保证安全,不一定保证顺序),但同步包含互斥
  3. 合理的同步策略可以解决单纯互斥可能导致的资源分配不均、执行流饥饿等问题。反之会加剧死锁或饥饿风险
  4. 同步的基本思想是:执行流先竞争 访问临界资源的资格(竞争锁、信号量),然后再根据资源状态决定是立即访问还是排队等待

下面介绍两种实现同步机制的工具:条件变量与信号量

竞态条件是指多个执行流并发访问和操作共享资源时,由于执行顺序的不确定性,导致程序最终结果依赖于执行流相对时序的一种错误状态。其发生需要三个必要条件:存在共享资源、多个执行流并发访问、且至少一个执行流修改资源。竞态条件的根本原因是操作的非原子性------多个执行流的"读-改-写"等复合操作可能被交错执行,导致最终状态不符合预期,表现为数据不一致、程序崩溃或安全漏洞等问题
互斥管的是能不能同时做(权限);同步管的是什么时候能做(时机)

竞态条件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"操作是原子的,无竞态条件
阻塞行为 可能被信号中断,此时返回 -1errno 设为 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循环
相关推荐
学Linux的语莫1 小时前
k8s常用命令
linux·容器·kubernetes
openKylin1 小时前
《2025年度OpenAtom openKylin社区全景案例集》正式发布
linux
CS_Zero1 小时前
Ubuntu安装Claude Code
linux·ubuntu·ai编程·claude
A星空1232 小时前
三、Kconfig介绍以及制作menuconfig界面
linux·运维·服务器
zylyehuo3 小时前
Windows & Linux 双系统资料整理
linux·夯实基础
口袋物联4 小时前
模板方法模式在 C 语言中的应用(含 Linux 内核实例)
linux·c语言·模板方法模式
一个人旅程~5 小时前
Linux Fcitx5输入法这么难念的由来?
linux·经验分享·电脑·ai写作
开开心心就好5 小时前
一键加密隐藏视频,专属格式播放工具
java·linux·开发语言·网络·人工智能·macos
小心草里有鬼6 小时前
VMware虚拟机扩容
linux·后端·centos·vim