Linux中用于线程/进程同步的核心函数——`sem_wait`函数
<摘要>
sem_wait
是 POSIX 信号量操作函数,用于对信号量执行 P 操作(等待、获取)。它的核心功能是原子地将信号量的值减 1。如果信号量的值大于 0,则减 1 并立即返回;如果信号量的值为 0,则调用线程(或进程)会被阻塞,直到另一个线程执行 sem_post
增加信号量值后将其唤醒,或者被信号中断。它主要用于保护共享资源(实现互斥锁)或协调线程/进程间的执行顺序(同步),是构建并发程序的基石之一。
<解析>
你可以把信号量想象成一个令牌桶,而 sem_wait
就是获取令牌的操作。
- 桶里有令牌(信号量值 > 0):你拿走一个,继续工作。
- 桶里没令牌(信号量值 = 0):你必须等待,直到有人往桶里还回令牌(
sem_post
),你才能拿走一个并继续。
1) 函数的概念与用途
- 功能:原子地减少(锁定)一个信号量的值。如果该操作会导致信号量值为负,则阻塞调用者。
- 场景:
- 互斥(Mutex):初始化信号量为 1。线程在访问临界区(共享资源)前调用
sem_wait
,离开后调用sem_post
。这确保了任何时候只有一个线程在临界区内。 - 同步(Sync):初始化信号量为 0。用于协调线程间的执行顺序。例如,线程 A 必须等待线程 B 完成某项工作后才能继续,那么线程 A 会
sem_wait
在一个信号量上,而线程 B 完成后调用sem_post
来唤醒 A。 - 控制资源访问数量:初始化信号量为 N(如数据库连接池大小)。线程访问资源前
sem_wait
,用完后再sem_post
,从而将并发访问数控制在 N 以内。
- 互斥(Mutex):初始化信号量为 1。线程在访问临界区(共享资源)前调用
2) 函数的声明与出处
sem_wait
定义在 <semaphore.h>
头文件中,是 POSIX 线程库的一部分,链接时需要 -pthread
选项。
int sem_wait(sem_t *sem);
3) 返回值的含义与取值范围
- 成功:返回
0
。 - 失败:返回
-1
,并设置相应的错误码errno
。EINVAL
:参数sem
不是有效的信号量指针。EINTR
:此调用被信号中断。这是一个非常重要且常见的情况。阻塞中的sem_wait
可以被信号处理函数打断,此时它会返回-1
并设置errno
为EINTR
。健壮的程序需要检查并处理这种情况。
4) 参数的含义与取值范围
sem_t *sem
- 作用:指向一个已初始化信号量的指针。
- 取值范围:必须是一个由
sem_init
或sem_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
注意事项:
- 链接选项:使用
sem_*
系列函数时,必须在编译时加上-pthread
链接选项,否则可能链接失败或产生不可预知的行为。 - 初始化:必须在使用信号量之前对其进行初始化(
sem_init
用于进程内线程间,sem_open
用于进程间)。 - EINTR 处理:阻塞的
sem_wait
可以被信号中断。编写健壮的程序时必须考虑这种情况,通常使用循环来重试。示例 3 展示了最佳实践。 - 销毁与清理:动态初始化的信号量(
sem_init
)在使用完毕后应使用sem_destroy
进行销毁以释放资源。 - 不可用于文件操作:
sem_t
对象是内存中的结构体,不能直接用于read
/write
等文件操作。命名信号量(sem_open
)有持久化语义,但操作仍需通过专门的函数。 - 与互斥锁的区别:信号量更通用。互斥锁(
pthread_mutex_t
)可视为初始值为 1 的信号量,但互斥锁有所有权概念(必须由锁定的线程解锁),而信号量没有。
7) 执行结果说明
- 示例1:运行后,最终打印的
Actual final value
会精确地等于Expected final value
(200000)。如果移除sem_wait
和sem_post
调用,由于竞态条件,实际值通常会远小于预期值。这证明了信号量成功保护了共享变量。 - 示例2:运行后,你会看到生产和消费交替进行的日志。由于生产者生产速度快于消费者,生产者最终会因缓冲区满而阻塞(
sem_wait(&empty_slots)
),等待消费者消费。这展示了信号量如何协调不同速度的线程。 - 示例3:运行后,程序会阻塞在
robust_sem_wait
中。此时按下Ctrl+C
发送SIGINT
信号,你会看到信号处理函数打印信息,并且sem_wait
调用被中断后重试的日志。程序会继续阻塞,直到你用其他方式(如另一个终端)无法增加信号量值。这演示了如何优雅地处理信号中断。