<摘要>
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
調用被中斷后重試的日志。程序會繼續阻塞,直到你用其他方式(如另一個終端)無法增加信號量值。這演示了如何優雅地處理信號中斷。