訊號量、同步這些名詞在處理序間通訊時就已經說過,在這裡它們的意思是相同的,只不過是同步的對象不同而已。但是下面介紹的訊號量的介面是用於線程的訊號量,注意不要跟用於處理序間通訊的訊號量混淆。
一、什麼是訊號量
線程的訊號量與處理序間通訊中使用的訊號量的概念是一樣,它是一種特殊的變數,它可以被增加或減少,但對其的關鍵訪問被保證是原子操作。如果一個程式中有多個線程試圖改變一個訊號量的值,系統將保證所有的操作都將依次進行。
而只有0和1兩種取值的訊號量叫做二進位訊號量,在這裡將重點介紹。而訊號量一般常用於保護一段代碼,使其每次只被一個執行線程運行。我們可以使用二進位訊號量來完成這個工作。
二、訊號量的介面和使用
訊號量的函數都以sem_開頭,線程中使用的基本訊號量函數有4個,它們都聲明在標頭檔semaphore.h中。
1、sem_init函數
該函數用於建立訊號量,其原型如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
該函數初始化由sem指向的訊號對象,設定它的共用選項,並給它一個初始的整數值。pshared控制訊號量的類型,如果其值為0,就表示這個訊號量是當前進程的局部訊號量,否則訊號量就可以在多個進程之間共用,value為sem的初始值。調用成功時返回0,失敗返回-1.
2、sem_wait函數
該函數用於以原子操作的方式將訊號量的值減1。原子操作就是,如果兩個線程企圖同時給一個訊號量加1或減1,它們之間不會互相干擾。它的原型如下:
int sem_wait(sem_t *sem);
sem指向的對象是由sem_init調用初始化的訊號量。調用成功時返回0,失敗返回-1.
3、sem_post函數
該函數用於以原子操作的方式將訊號量的值加1。它的原型如下:
int sem_post(sem_t *sem);
與sem_wait一樣,sem指向的對象是由sem_init調用初始化的訊號量。調用成功時返回0,失敗返回-1.
4、sem_destroy函數
該函數用於對用完的訊號量的清理。它的原型如下:
int sem_destroy(sem_t *sem);
成功時返回0,失敗時返回-1.
三、使用訊號量同步線程
下面以一個簡單的多線程程式來說明如何使用訊號量進行線程同步。在主線程中,我們建立子線程,並把數組msg作為參數傳遞給子線程,然後主線程等待直到有文本輸入,然後調用sem_post來增加訊號量的值,這樣就會立刻使子線程從sem_wait的等待中返回並開始執行。線程函數在把字串的小寫字母變成大寫並統計輸入的字元數量之後,它再次調用sem_wait並再次被阻塞,直到主線程再次調用sem_post增加訊號量的值。
#include <unistd.h> #include <pthread.h> #include <semaphore.h> #include <stdlib.h> #include <stdio.h> #include <string.h> //線程函數 void *thread_func(void *msg); sem_t sem;//訊號量 #define MSG_SIZE 512 int main() { int res = -1; pthread_t thread; void *thread_result = NULL; char msg[MSG_SIZE]; //初始化訊號量,其初值為0 res = sem_init(&sem, 0, 0); if(res == -1) { perror("semaphore intitialization failed\n"); exit(EXIT_FAILURE); } //建立線程,並把msg作為線程函數的參數 res = pthread_create(&thread, NULL, thread_func, msg); if(res != 0) { perror("pthread_create failed\n"); exit(EXIT_FAILURE); } //輸入資訊,以輸入end結束,由於fgets會把斷行符號(\n)也讀入,所以判斷時就變成了“end\n” printf("Input some text. Enter 'end'to finish...\n"); while(strcmp("end\n", msg) != 0) { fgets(msg, MSG_SIZE, stdin); //把訊號量加1 sem_post(&sem); } printf("Waiting for thread to finish...\n"); //等待子線程結束 res = pthread_join(thread, &thread_result); if(res != 0) { perror("pthread_join failed\n"); exit(EXIT_FAILURE); } printf("Thread joined\n"); //清理訊號量 sem_destroy(&sem); exit(EXIT_SUCCESS); } void* thread_func(void *msg) { //把訊號量減1 sem_wait(&sem); char *ptr = msg; while(strcmp("end\n", msg) != 0) { int i = 0; //把小寫字母變成大寫 for(; ptr[i] != '\0'; ++i) { if(ptr[i] >= 'a' && ptr[i] <= 'z') { ptr[i] -= 'a' - 'A'; } } printf("You input %d characters\n", i-1); printf("To Uppercase: %s\n", ptr); //把訊號量減1 sem_wait(&sem); } //退出線程 pthread_exit(NULL); }
運行結果如下:
從啟動並執行結果來看,這個程式的確是同時在運行兩個線程,一個控制輸入,另一個控制處理統計和輸出。
四、分析此訊號量同步程式的缺陷
但是這個程式有一點點的小問題,就是這個程式依賴接收文本輸入的時間足夠長,這樣子線程才有足夠的時間在主線程還未準備好給它更多的單詞去處理和統計之前處理和統計出工作區中字元的個數。所以當我們連續快速地給它兩組不同的單詞去統計時,子線程就沒有足夠的時間支執行,但是訊號量已被增加不止一次,所以字元統計線程(子線程)就會反覆處理和統計字元數目,並減少訊號量的值,直到它再次變成0為止。
為了更加清楚地說明上面所說的情況,修改主線程的while迴圈中的代碼,如下:
printf("Input some text. Enter 'end'to finish...\n"); while(strcmp("end\n", msg) != 0) { if(strncmp("TEST", msg, 4) == 0) { strcpy(msg, "copy_data\n"); sem_post(&sem); } fgets(msg, MSG_SIZE, stdin); //把訊號量加1 sem_post(&sem); }
重新編譯器,此時運行結果如下:
當我們輸入TEST時,主線程向子線程提供了兩個輸入,一個是來自鍵盤的輸入,一個來自主線程複資料到msg中,然後從運行結果可以看出,運行出現了異常,沒有處理和統計從鍵盤輸入TEST的字串而卻對複製的資料作了兩次處理。原因如上面所述。
五、解決此缺陷的方法
解決方案有兩個,一個就是再增加一個訊號量,讓主線程等到子線程處理統計完成之後再繼續執行;另一個方法就是使用互斥量。
下面給出用增加一個訊號量的方法來解決該問題的代碼,源檔案名稱為semthread2.c,原始碼如下:
#include <unistd.h> #include <pthread.h> #include <semaphore.h> #include <stdlib.h> #include <stdio.h> #include <string.h> //線程函數 void *thread_func(void *msg); sem_t sem;//訊號量 sem_t sem_add;//增加的訊號量 #define MSG_SIZE 512 int main() { int res = -1; pthread_t thread; void *thread_result = NULL; char msg[MSG_SIZE]; //初始化訊號量,初始值為0 res = sem_init(&sem, 0, 0); if(res == -1) { perror("semaphore intitialization failed\n"); exit(EXIT_FAILURE); } //初始化訊號量,初始值為1 res = sem_init(&sem_add, 0, 1); if(res == -1) { perror("semaphore intitialization failed\n"); exit(EXIT_FAILURE); } //建立線程,並把msg作為線程函數的參數 res = pthread_create(&thread, NULL, thread_func, msg); if(res != 0) { perror("pthread_create failed\n"); exit(EXIT_FAILURE); } //輸入資訊,以輸入end結束,由於fgets會把斷行符號(\n)也讀入,所以判斷時就變成了“end\n” printf("Input some text. Enter 'end'to finish...\n"); sem_wait(&sem_add); while(strcmp("end\n", msg) != 0) { if(strncmp("TEST", msg, 4) == 0) { strcpy(msg, "copy_data\n"); sem_post(&sem); //把sem_add的值減1,即等待子線程處理完成 sem_wait(&sem_add); } fgets(msg, MSG_SIZE, stdin); //把訊號量加1 sem_post(&sem); //把sem_add的值減1,即等待子線程處理完成 sem_wait(&sem_add); } printf("Waiting for thread to finish...\n"); //等待子線程結束 res = pthread_join(thread, &thread_result); if(res != 0) { perror("pthread_join failed\n"); exit(EXIT_FAILURE); } printf("Thread joined\n"); //清理訊號量 sem_destroy(&sem); sem_destroy(&sem_add); exit(EXIT_SUCCESS); } void* thread_func(void *msg) { char *ptr = msg; //把訊號量減1 sem_wait(&sem); while(strcmp("end\n", msg) != 0) { int i = 0; //把小寫字母變成大寫 for(; ptr[i] != '\0'; ++i) { if(ptr[i] >= 'a' && ptr[i] <= 'z') { ptr[i] -= 'a' - 'A'; } } printf("You input %d characters\n", i-1); printf("To Uppercase: %s\n", ptr); //把訊號量加1,表明子線程處理完成 sem_post(&sem_add); //把訊號量減1 sem_wait(&sem); } sem_post(&sem_add); //退出線程 pthread_exit(NULL);
其運行結果如下:
分析:這裡我們多使用了一個訊號量sem_add,並把它的初值賦為1,在主線程在使用sem_wait來等待子線程處理完全,由於它的初值為1,所以主線程第一次調用sem_wait總是立即返回,而第二次調用則需要等待子線程處理完成之後。而在子線程中,若處理完成就會馬上使用sem_post來增加訊號量的值,使主線程中的sem_wait馬上返回並執行緊接下面的代碼。從運行結果來看,運行終於正常了。注意,線上程函數中,訊號量sem和sem_add使用sem_wait和sem_post函數的次序,它們的次序不能錯亂,否則在輸入end時,可能運行不正常,子線程不能正常退出,從而導致程式不能退出。
以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。