信号量、条件量

POSIX信号量

基本概念

POSIX信号量与IPC信号量组的核心逻辑一致,但操作更简便、接口更易用,是多进程/多线程并发编程中的常用同步工具。

信号量分类

  • POSIX匿名信号量
    • 适用场景:线程间同步
    • 存储特性:仅存在于内存,文件系统中不可见
  • POSIX具名信号量
    • 适用场景:进程间同步
    • 存储特性:存在于文件系统 /dev/shm 目录,可被任意有权限的进程访问

核心:PV操作

    对于信号量而言,最重要的作用就是用来表征对应资源的数量,所谓的P/V操作就是对资源数量进行 +1/-1 操作,既然只是个加减法,那么为什么不使用普通的整型数据呢?原因是:

  • 整型数据的加减操作不具有原子性,即操作可能被中断
  • 普通加减法无法提供阻塞特性,而申请资源不可得时应进入阻塞

    对于原子性再做个简单的解释,即这种资源数量的加减法不能有中间过程,不管是成功还是失败都必须一次性完成。加减法看似简单,但在硬件层面上并非一个原子性操作,而是包含了多个寄存器操作步骤,因此不能作为P/V操作的手段。

1.P操作是申请资源,因此如果资源数不够的话会导致进程阻塞,这正是我们想要的效果,因为资源数不可为负数。
2.V操作是释放资源,永远不会阻塞。
3.SEM对象的一大特色就是可以对多个信号量元素同时进行P/V操作,这也是跟POSIX单个信号量的区别。

    如果需要使用信号量来实现进程间的同步操作,则可以使用PV来实现,比如A进程运行成功后会释放一个信号量s1 ,二进程B 则在运行前需要得到信号量 s1 ,这么一来不管AB两个进程谁先运行,我们都可以确定B进行在进行某项操作前必须得到A进程的信号。

为何不用普通整型

  • 普通整型加减不具备原子性,硬件层面需多步寄存器操作,可能被中断
  • 普通加减法无阻塞特性,资源申请失败时无法让进程/线程进入等待状态

PV操作定义

  1. P操作(申请资源):资源数减1,若资源不足则阻塞,确保资源数不为负
  2. V操作(释放资源):资源数加1,永远不会阻塞
  3. 补充区别:IPC信号量组可对多个信号量元素同时执行PV操作,POSIX信号量仅支持单个操作

PV操作的同步作用

通过PV操作可实现进程/线程间的顺序控制。例如:

  • 进程A执行成功后,通过V操作释放信号量s1
  • 进程B执行前需通过P操作获取信号量s1
  • 无论A、B启动顺序如何,都能保证B的目标操作在A之后执行

匿名信号量操作流程

  1. 定义信号量:可存储在栈、堆或数据段,确保多个任务能便捷访问
  2. 初始化信号量
  3. 执行PV操作
  4. 销毁信号量

匿名信号量核心接口

初始化与销毁

#include <semaphore.h>
int sem_destroy(sem_t *sem);  // 销毁信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);  // 初始化信号量

参数说明:

  • sem:待初始化/销毁的目标信号量指针
  • pshared:是否支持进程间使用(0=线程间,非0=进程间,需确保信号量可被多进程直接访问)
  • value:信号量初始值,表征初始资源数量

V操作(释放资源)

#include <semaphore.h>
int sem_post(sem_t *sem);  // 资源数+1,无阻塞

P操作(申请资源)

#include <semaphore.h>
int sem_wait(sem_t *sem);  // 阻塞式申请,资源不足时休眠
int sem_trywait(sem_t *sem);  // 非阻塞式申请,资源不足时直接返回错误
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout);  // 限时阻塞申请

具名信号量

核心特性

  • 适用场景:多进程间同步互斥
  • PV操作逻辑:与匿名信号量完全一致
  • 关键区别:存在于 /dev/shm 文件系统,可被任意有权限的进程打开访问

创建与打开接口

#include <fcntl.h>           /* 包含O_*常量定义 */
#include <sys/stat.h>        /* 包含mode常量定义 */
#include <semaphore.h>

// 两种版本,类似文件open()函数
sem_t *sem_open(const char *name, int oflag);  // 后续打开已创建的信号量
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);  // 首次创建时使用

参数说明:

  • name:信号量名称(文件系统中可见的标识)
  • oflag:打开标志(如O_CREAT表示创建)
  • mode:文件权限(首次创建时必须指定)
  • value:信号量初始值(首次创建时必须指定)

关闭与删除接口

#include <semaphore.h>
int sem_close(sem_t *sem);  // 关闭信号量,释放内核维护的资源
int sem_unlink(const char *name);  // 删除信号量对应的文件,彻底清理资源

注意事项

  • 具名信号量关闭后,文件不会自动消失,其值会保留所有PV操作结果
  • 例:连续执行3次释放操作(V),信号量值会+3,后续可执行3次申请操作(P)
  • 即使所有进程退出,信号量文件仍存在,需通过 sem_unlink() 或文件系统手动删除

拓展知识:信号量常见应用场景

  • 资源池管理:限制同时访问某资源的进程/线程数量(如数据库连接池)
  • 生产者-消费者模型:平衡生产和消费速度,避免缓冲区溢出或空读
  • 临界区保护:确保同一时间只有一个进程/线程访问临界资源(二值信号量)

条件量

基本概念

    在许多场合中,程序的执行通常需要满足一定的条件,条件不成熟的时候,任务应该进入睡眠阻塞等待,条件成熟时应该可以被快速唤醒(异步)。另外,在并发程序中,会与其他任务同时访问该条件,因此任何时候都必须以互斥的方式对条件进行访问。条件量就是专门解决上述场景的逻辑机制。
    注意,上述表述中,条件和条件量是两个不同的东西,所谓条件就是指程序要继续运行所需要的前提条件,比如文件是否读完、内存是否清空等具体的场景限定,而条件量(即pthread_cond_t)是本节课件要讨论的一种同步互斥变量,专用于解决上述逻辑场景。

关键术语区分

  • 条件:程序继续执行的前提(如文件读完、内存清空等具体场景)
  • 条件量:pthread_cond_t 类型变量,提供阻塞睡眠和唤醒功能

核心执行逻辑

逻辑图

说明:
1.在进行条件判断前(判断的目标一般是临界资源),先加锁(防止其他任务并发访问)
2.成功加锁后,判断条件是否允许

  • 若条件允许,则直接操作临界资源,然后释放锁
  • 若条件不允许,则进入条件量的等待队列中睡眠,并同时释放锁

3.在条件量中睡眠的任务,可以被其他任务唤醒,唤醒时重新判定条件是否允许程序继续执行,当然也是必须先加锁。

核心特性

  • 必须与互斥锁(或二值信号量)配套使用
  • 互斥锁负责临界资源的互斥访问,条件量负责阻塞和唤醒
  • 支持异步唤醒,条件成熟时可快速响应

操作流程与核心接口

定义与初始化

#include <pthread.h>

// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 动态销毁
int pthread_cond_destroy(pthread_cond_t *cond);
// 静态初始化(更简洁)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

参数说明:

  • cond:待初始化/销毁的条件量指针
  • attr:条件量属性,NULL表示使用默认属性

阻塞等待接口

#include <pthread.h>

// 无限期阻塞等待
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 限时阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

参数说明:

  • cond:目标条件量,条件不满足时进入等待队列
  • mutex:需释放的互斥锁,唤醒时自动重新获取
  • abstime:等待的绝对时间(超时后自动返回)

唤醒接口

#include <pthread.h>

// 广播唤醒:唤醒所有等待该条件量的任务
int pthread_cond_broadcast(pthread_cond_t *cond);
// 单播唤醒:唤醒任意一个等待该条件量的任务
int pthread_cond_signal(pthread_cond_t *cond);

拓展:条件量常见错误与避免

  • 错误1:未加锁直接判断条件 → 避免:始终在互斥锁保护下判断条件
  • 错误2:持有互斥锁进入等待 → 避免:pthread_cond_wait() 会自动释放锁,无需手动解锁
  • 错误3:唤醒后未重新判断条件 → 避免:唤醒后可能条件已变化,需再次检查
  • 错误4:使用pthread_cond_signal()唤醒多个等待任务 → 避免:需唤醒多个时用pthread_cond_broadcast()

信号量与条件量对比

特性 信号量 条件量
核心作用 管理资源数量,实现同步互斥 基于条件的阻塞/唤醒,实现同步
依赖组件 无需额外依赖(二值信号量可模拟锁) 必须与互斥锁配套使用
适用场景 资源计数、临界区保护、进程/线程同步 条件触发型场景(如生产者-消费者、等待事件)
阻塞触发条件 资源不足 条件不满足
唤醒方式 释放资源(V操作)自动唤醒 需显式调用signal/broadcast接口

posted @ 2025-12-04 08:21  Jaklin  阅读(40)  评论(0)    收藏  举报