当前位置: 首页 > news >正文

Linux中用于线程/进程同步的核心函数——`sem_wait`函数

<摘要>

sem_wait 是 POSIX 信号量操作函数,用于对信号量执行 P 操作(等待、获取)。它的核心功能是原子地将信号量的值减 1。如果信号量的值大于 0,则减 1 并立即返回;如果信号量的值为 0,则调用线程(或进程)会被阻塞,直到另一个线程执行 sem_post 增加信号量值后将其唤醒,或者被信号中断。它主要用于保护共享资源(实现互斥锁)或协调线程/进程间的执行顺序(同步),是构建并发程序的基石之一。


<解析>

你可以把信号量想象成一个令牌桶,而 sem_wait 就是获取令牌的操作。

  • 桶里有令牌(信号量值 > 0):你拿走一个,继续工作。
  • 桶里没令牌(信号量值 = 0):你必须等待,直到有人往桶里还回令牌(sem_post),你才能拿走一个并继续。
1) 函数的概念与用途
  • 功能:原子地减少(锁定)一个信号量的值。如果该操作会导致信号量值为负,则阻塞调用者。
  • 场景
    1. 互斥(Mutex):初始化信号量为 1。线程在访问临界区(共享资源)前调用 sem_wait,离开后调用 sem_post。这确保了任何时候只有一个线程在临界区内。
    2. 同步(Sync):初始化信号量为 0。用于协调线程间的执行顺序。例如,线程 A 必须等待线程 B 完成某项工作后才能继续,那么线程 A 会 sem_wait 在一个信号量上,而线程 B 完成后调用 sem_post 来唤醒 A。
    3. 控制资源访问数量:初始化信号量为 N(如数据库连接池大小)。线程访问资源前 sem_wait,用完后再 sem_post,从而将并发访问数控制在 N 以内。
2) 函数的声明与出处

sem_wait 定义在 <semaphore.h> 头文件中,是 POSIX 线程库的一部分,链接时需要 -pthread 选项。

int sem_wait(sem_t *sem);
3) 返回值的含义与取值范围
  • 成功:返回 0
  • 失败:返回 -1,并设置相应的错误码 errno
    • EINVAL:参数 sem 不是有效的信号量指针。
    • EINTR此调用被信号中断。这是一个非常重要且常见的情况。阻塞中的 sem_wait 可以被信号处理函数打断,此时它会返回 -1 并设置 errnoEINTR。健壮的程序需要检查并处理这种情况。
4) 参数的含义与取值范围
  1. sem_t *sem
    • 作用:指向一个已初始化信号量的指针。
    • 取值范围:必须是一个由 sem_initsem_open 初始化/创建的有效信号量对象的地址。
5) 函数使用案例

示例 1:用信号量实现互斥锁(保护共享变量)
此示例展示两个线程如何通过信号量安全地增加一个共享计数器。

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>#define NUM_OPERATIONS 100000int shared_counter = 0;
sem_t counter_sem; // 信号量,用于保护 shared_countervoid* increment_counter(void* arg) {for (int i = 0; i < NUM_OPERATIONS; ++i) {// 进入临界区前获取信号量 (P操作)if (sem_wait(&counter_sem) != 0) {perror("sem_wait failed");return NULL;}// 临界区开始shared_counter++; // 这是一个非原子操作,需要保护// 临界区结束// 离开临界区后释放信号量 (V操作)if (sem_post(&counter_sem) != 0) {perror("sem_post failed");return NULL;}}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化一个用于互斥的信号量,初始值为 1if (sem_init(&counter_sem, 0, 1) != 0) {perror("sem_init failed");return 1;}// 创建两个线程if (pthread_create(&thread1, NULL, increment_counter, NULL) != 0 ||pthread_create(&thread2, NULL, increment_counter, NULL) != 0) {perror("pthread_create failed");return 1;}// 等待两个线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁信号量sem_destroy(&counter_sem);// 理论上最终结果应该是 2 * NUM_OPERATIONSprintf("Expected final value: %d\n", 2 * NUM_OPERATIONS);printf("Actual final value: %d\n", shared_counter);// 如果没有信号量保护,实际值通常会小于预期值 due to race conditionsreturn 0;
}

示例 2:用信号量实现线程同步(生产者-消费者模型)
此示例展示一个简单的单生产者-单消费者模型,使用两个信号量来同步生产和消费的顺序。

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>#define BUFFER_SIZE 5int buffer[BUFFER_SIZE];
int in = 0, out = 0;sem_t empty_slots; // 计数空槽位的信号量
sem_t full_slots;  // 计数已填充槽位的信号量void* producer(void* arg) {int item = 0;for (int i = 0; i < 10; ++i) {item = i; // 生产一个物品// 等待一个空槽位 (P(empty))sem_wait(&empty_slots);// 临界区:将物品放入缓冲区buffer[in] = item;printf("Produced: %d at index %d\n", item, in);in = (in + 1) % BUFFER_SIZE;// 临界区结束// 通知消费者多了一个满槽位 (V(full))sem_post(&full_slots);// 模拟生产时间sleep(1);}return NULL;
}void* consumer(void* arg) {int item;for (int i = 0; i < 10; ++i) {// 等待一个满槽位 (P(full))sem_wait(&full_slots);// 临界区:从缓冲区取出物品item = buffer[out];printf("Consumed: %d from index %d\n", item, out);out = (out + 1) % BUFFER_SIZE;// 临界区结束// 通知生产者多了一个空槽位 (V(empty))sem_post(&empty_slots);// 模拟消费处理时间sleep(2);}return NULL;
}int main() {pthread_t prod_thread, cons_thread;// 初始化信号量// 开始时所有槽位都是空的sem_init(&empty_slots, 0, BUFFER_SIZE);// 开始时没有已填充的槽位sem_init(&full_slots, 0, 0);pthread_create(&prod_thread, NULL, producer, NULL);pthread_create(&cons_thread, NULL, consumer, NULL);pthread_join(prod_thread, NULL);pthread_join(cons_thread, NULL);sem_destroy(&empty_slots);sem_destroy(&full_slots);printf("Producer-Consumer simulation finished.\n");return 0;
}

示例 3:处理 sem_wait 被信号中断(EINTR)
此示例展示如何编写健壮的代码来处理 sem_wait 被信号中断的情况。

#include <stdio.h>
#include <semaphore.h>
#include <signal.h>
#include <errno.h>sem_t demo_sem;void signal_handler(int sig) {printf("Signal %d received.\n", sig);// 信号处理函数不做复杂操作,只是打断阻塞调用
}int robust_sem_wait(sem_t *sem) {int ret;// 使用循环来重试被信号中断的 sem_waitwhile ((ret = sem_wait(sem)) == -1 && errno == EINTR) {// 如果失败原因是 EINTR,则继续重试printf("sem_wait was interrupted by a signal. Retrying...\n");continue;}return ret;
}int main() {// 设置信号处理函数 (例如 SIGINT: Ctrl+C)signal(SIGINT, signal_handler);// 初始化一个值为0的信号量,这样 sem_wait 会阻塞sem_init(&demo_sem, 0, 0);printf("Press Ctrl+C to interrupt the blocked sem_wait call.\n");printf("Calling sem_wait (will block)...\n");// 使用 robust_sem_wait 而不是直接的 sem_waitif (robust_sem_wait(&demo_sem) == 0) {printf("sem_wait succeeded!\n");} else {// 处理其他错误perror("robust_sem_wait failed with unexpected error");}sem_destroy(&demo_sem);return 0;
}
6) 编译方式与注意事项

编译命令(必须链接 pthread 库):

# 编译示例1和2
gcc -pthread -o sem_mutex sem_mutex.c
gcc -pthread -o sem_producer_consumer sem_producer_consumer.c
gcc -pthread -o sem_eintr sem_eintr.c

注意事项:

  1. 链接选项:使用 sem_* 系列函数时,必须在编译时加上 -pthread 链接选项,否则可能链接失败或产生不可预知的行为。
  2. 初始化:必须在使用信号量之前对其进行初始化(sem_init 用于进程内线程间,sem_open 用于进程间)。
  3. EINTR 处理阻塞的 sem_wait 可以被信号中断。编写健壮的程序时必须考虑这种情况,通常使用循环来重试。示例 3 展示了最佳实践。
  4. 销毁与清理:动态初始化的信号量(sem_init)在使用完毕后应使用 sem_destroy 进行销毁以释放资源。
  5. 不可用于文件操作sem_t 对象是内存中的结构体,不能直接用于 read/write 等文件操作。命名信号量(sem_open)有持久化语义,但操作仍需通过专门的函数。
  6. 与互斥锁的区别:信号量更通用。互斥锁(pthread_mutex_t)可视为初始值为 1 的信号量,但互斥锁有所有权概念(必须由锁定的线程解锁),而信号量没有。
7) 执行结果说明
  • 示例1:运行后,最终打印的 Actual final value 会精确地等于 Expected final value(200000)。如果移除 sem_waitsem_post 调用,由于竞态条件,实际值通常会远小于预期值。这证明了信号量成功保护了共享变量。
  • 示例2:运行后,你会看到生产和消费交替进行的日志。由于生产者生产速度快于消费者,生产者最终会因缓冲区满而阻塞(sem_wait(&empty_slots)),等待消费者消费。这展示了信号量如何协调不同速度的线程。
  • 示例3:运行后,程序会阻塞在 robust_sem_wait 中。此时按下 Ctrl+C 发送 SIGINT 信号,你会看到信号处理函数打印信息,并且 sem_wait 调用被中断后重试的日志。程序会继续阻塞,直到你用其他方式(如另一个终端)无法增加信号量值。这演示了如何优雅地处理信号中断。
8) 图文总结:sem_wait 工作流程

在这里插入图片描述


文章转载自:

http://PbQMmsOz.wxcsm.cn
http://nOGfSXK2.wxcsm.cn
http://8cbQFVAQ.wxcsm.cn
http://lzz60aR2.wxcsm.cn
http://InCIiqHS.wxcsm.cn
http://zvSK8dyh.wxcsm.cn
http://LPf42zL5.wxcsm.cn
http://Dr8bBoap.wxcsm.cn
http://xSFcBaZA.wxcsm.cn
http://ZPnpMl5g.wxcsm.cn
http://lqM35qsn.wxcsm.cn
http://KykbwQzM.wxcsm.cn
http://IK0ElcOs.wxcsm.cn
http://4DMClxfV.wxcsm.cn
http://ydfBI0Zr.wxcsm.cn
http://OxRSzHwU.wxcsm.cn
http://tGaPvTsD.wxcsm.cn
http://haIPP9aq.wxcsm.cn
http://L5CwKNT4.wxcsm.cn
http://C8DgsZtt.wxcsm.cn
http://uti6qpFU.wxcsm.cn
http://tYlovO9v.wxcsm.cn
http://xzk6dfQX.wxcsm.cn
http://s41jdwDf.wxcsm.cn
http://QKOefEKN.wxcsm.cn
http://4fru2YhB.wxcsm.cn
http://DCOEd3Fo.wxcsm.cn
http://QPcE74f2.wxcsm.cn
http://Zh8xQcBA.wxcsm.cn
http://Y8ZgatQJ.wxcsm.cn
http://www.dtcms.com/a/367740.html

相关文章:

  • Day2p2 夏暮客的Python之路
  • C++虚函数虚析构函数纯虚函数的使用说明和理解
  • Process Explorer 学习笔记(第三章3.1.1):度量 CPU 的使用情况详解
  • 机器学习入门,第一个MCP示例
  • Spring Boot项目中MySQL索引失效的常见场景与解决方案
  • 2025 年高教社杯全国大学生数学建模竞赛C 题 NIPT 的时点选择与胎儿的异常判定 完整成品思路模型代码分享,全网首发高质量!!!
  • 代码随想录学习摘抄day6(二叉树1-11)
  • 吴恩达机器学习(五)
  • Web 与 Nginx 网站服务:从基础到实践
  • 为什么打印出来的 cJSON type 值和头文件定义的不一样?
  • MySQL子查询的分类讲解与实战
  • 【蓝桥杯选拔赛真题64】C++最大空白区 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解
  • 企业中团队最常使用的git命令操作
  • MCP 和 Fuction Call 有什么不同
  • 去中心化投票系统开发教程 第一章:区块链基础知识
  • 热门盘点|政务办公移动化:开启政务服务高效协同新时代
  • ICPC Central Russia Regional Contest, 2024
  • (A题|烟幕干扰弹的投放策略)2025年高教杯全国大学生数学建模国赛解题思路|完整代码论文集合
  • 化工行业的设备管理软件应用_HawkEye智能运维平台_璞华大数据
  • 论文介绍:Fast3R,更快的多视图 3D 重建的新范式
  • Java 流程控制:从入门到面试的全方位指南
  • 嵌入式第四十六天(51单片机)
  • Dubbo消费者无法找到提供者问题分析和处理
  • ​​Nginx高性能Web服务器实战:从协议原理到运维优化​​
  • 【ffmepg+ AI 】从mp3歌曲提取伴奏(纯音乐)
  • TreeMap 和 LinkedHashMap原理介绍
  • 手写智能指针:带你彻底搞懂 C++ 内存管理的底层逻辑
  • MySQL中CASE语法规则的详细解析及扩展示例
  • 基于单片机楼宇火灾检测系统/仓库火灾检测报警系统
  • 基础crud项目(前端部分+总结)