本文目錄
- 一、線程概述
- 1.1 線程和進程的區別
- 1.2 線程之間共享和非共享資源
- 1.3 NPTL
- 二、線程操作
- 2.1 pthread_create
- 2.2 pthread_exit
- 2.3 pthread_join
- 2.4 pthread_detach
- 2.5 patch_cancel
- 2.6 pthread_attr
- 三、實戰demo
- 四、線程同步
- 五、死鎖
- 六、讀寫鎖
- 七、生產消費者模型
一、線程概述
與進程(process)類似,線程(thread)是允許應用程序并發執行多個任務的一種機制。一個進程可以包含多個線程。同一個程序中的所有線程均會獨立執行相同程序,且共享同一份全局內存區域,其中包括初始化數據段、未初始化數據段,以及堆內存段。(傳統意義上的 UNIX 進程只是多線程程序的一個特例,該進程只包含一個線程)進程是 CPU 分配資源的最小單位,線程是操作系統調度執行的最小單位。
線程是輕量級的進程(LWP:LightweightProcess),在inux環境下線程的本質仍是進程。
查看指定進程的 LWP 號:ps-Lf pid
。
比如說當在服務器中打開一個火狐瀏覽器后,可以通過命令ps aux
查看對應的進程號,然后通過ps -Lf <火狐進程號>
查看瀏覽器啟動之后的線程。
1.1 線程和進程的區別
進程間的信息難以共享,由于除去只讀代碼之外,父子進程并未共享內存,因此必須采用一些進程間的通信方式,才能在進程間進行信息交換。
調用fork()
創建進程的代價比較高,就算利用寫時復制的技術,任然需要復制內存頁表
和文件描述符表
之類的多種進程屬性,也就是fork的開銷其實還是不菲的。
線程之間能夠方便、快速的共享信息,只需將數據復制到共享(全局或者堆)變量
中即可。在虛擬地址空間的棧空間
中,每個子線程都有自己的一塊對應區域,.text
代碼段也是每個線程都有自己對應的。
創建線程 比進程通常快10倍不止,線程間是共享虛擬地址空間的,無需采用寫時復制,無需復制頁表。
1.2 線程之間共享和非共享資源
共享資源中,除了虛擬地址空間,其他的都是內核區的。
1.3 NPTL
當 Linux 最初開發時,在內核中并不能真正支持線程。但是它的確可以通過 clone() 系統調用將進程作為可調度的實體。這個調用創建了調用進程(caling process)的一個拷貝,這個拷貝與調用進程共享相同的地址空間。
LinuxThreads 項目使用這個調用來完成在用戶空間模擬對線程的支持。不幸的是,這種方法有一些缺點,尤其是在信號處理、調度和進程間同步等方面都存在問題。另外,這個線程模型也不符合 POSIX的要求。
要改進 LinuxThreads,需要內核的支持,并且重寫線程庫。有兩個相互競爭的項目開始來滿足這些要求。一個包括 IBM 的開發人員的團隊開展了 NGPT(Next-Generation POSIXThreads)項目。同時Red Hat 的一些開發人員開展了 NPTL 項目。NGPT 在 2003 年中期被放棄了,把這個領域完全留給了NPTL。
NPTL,或稱為 Native POSlX Thread Library,是 Linux線程的一個新實現,它克服了 LinuxThreads的缺點,同時也符合 POSIX的需求。與 LinuxThreads 相比,它在性能和穩定性方面都提供了重大的改進。査看當前 pthread 庫版本:
getconf GNU LIBPTHREAD VERSION
二、線程操作
pthread_t pthread_self(void);int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine) (void *), void *arg);int pthread_equal(pthread_t t1, pthread_t t2);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
int pthread_cancel(pthread_t thread);
2.1 pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);- 功能:創建一個子線程- 參數:- thread:傳出參數,線程創建成功后,子線程的線程ID被寫到該變量中。- attr : 設置線程的屬性,一般使用默認值,NULL- start_routine : 函數指針,這個函數是子線程需要處理的邏輯代碼- arg : 給第三個參數使用,傳參- 返回值:成功:0失敗:返回錯誤號。這個錯誤號和之前errno不太一樣。獲取錯誤號的信息: char * strerror(int errnum);
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("child thread...\n");printf("arg value: %d\n", *(int *)arg);return NULL;
}int main() {pthread_t tid;int num = 10;// 創建一個子線程 (void *)&num 是把num的地址轉換為void *類型,然后傳給子線程進行使用int ret = pthread_create(&tid, NULL, callback, (void *)&num);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);} for(int i = 0; i < 5; i++) {printf("%d\n", i);}sleep(1); //防止主線程過快結束,導致子線程沒有運行return 0; // exit(0);
}
如果直接用命令編譯: gcc pthread_create.c -o phread
, 會顯示如下報錯:
需要在命令的末尾加上-pthread
才能運行(可以查看man文檔)
也就是gcc pthread_create.c -o pthread -pthread
等價于gcc thread_create.c -o pthread -lpthread
(通過-lib指定pthread這個庫。)
2.2 pthread_exit
#include <pthread.h>
void pthread_exit(void *retval);功能:終止一個線程,在哪個線程中調用,就表示終止哪個線程參數:retval:需要傳遞一個指針,作為一個返回值,可以在pthread_join()中獲取到。pthread_t pthread_self(void);功能:獲取當前的線程的線程IDint pthread_equal(pthread_t t1, pthread_t t2);功能:比較兩個線程ID是否相等不同的操作系統,pthread_t類型的實現不一樣,有的是無符號的長整型,有的是使用結構體去實現的。
#include <stdio.h>
#include <pthread.h>
#include <string.h>void * callback(void * arg) {printf("child thread id : %ld\n", pthread_self());return NULL; // return NULL 等價于pthread_exit(NULL);
} int main() {// 創建一個子線程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}// 主線程for(int i = 0; i < 5; i++) {printf("%d\n", i);}printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());// 讓主線程退出,當主線程退出時,不會影響其他正常運行的線程。pthread_exit(NULL);printf("main thread exit\n");return 0; // exit(0);
}
2.3 pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);- 功能:和一個已經終止的線程進行連接回收子線程的資源這個函數是阻塞函數,調用一次只能回收一個子線程一般在主線程中使用- 參數:- thread:需要回收的子線程的ID- retval: 接收子線程退出時的返回值- 返回值:0 : 成功非0 : 失敗,返回的錯誤號
如果運行的value是全局變量,那么能夠順利回收資源。
但是如果采用的是下面代碼的pthread_exit((void *)&value); 這一行是問題的關鍵。value 是一個局部變量,它存儲在子線程的棧空間中。當子線程執行到 pthread_exit 時,value 的作用域已經結束,子線程的棧空間可能會被回收或覆蓋。因此,pthread_exit 返回的指針指向了一個已經被銷毀的局部變量的地址,這導致了未定義行為。所以回收了打印的value是亂值。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>//int value = 10;void * callback(void * arg) {printf("child thread id : %ld\n", pthread_self());sleep(3);// return NULL; int value = 10; // 局部變量pthread_exit((void *)&value); // return (void *)&value;
} int main() {// 創建一個子線程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}// 主線程for(int i = 0; i < 5; i++) {printf("%d\n", i);}printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());// 主線程調用pthread_join()回收子線程的資源int * thread_retval;ret = pthread_join(tid, (void **)&thread_retval);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}printf("exit data : %d\n", *thread_retval);printf("回收子線程資源成功!\n");// 讓主線程退出,當主線程退出時,不會影響其他正常運行的線程。pthread_exit(NULL);return 0;
}
2.4 pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);- 功能:分離一個線程。被分離的線程在終止的時候,會自動釋放資源返回給系統。1.不能多次分離,會產生不可預料的行為。2.不能去連接一個已經分離的線程,會報錯。- 參數:需要分離的線程的ID- 返回值:成功:0失敗:返回錯誤號
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());return NULL;
}int main() {// 創建一個子線程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 輸出主線程和子線程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());// 設置子線程分離,子線程分離后,子線程結束時對應的資源就不需要主線程釋放ret = pthread_detach(tid);if(ret != 0) {char * errstr = strerror(ret);printf("error2 : %s\n", errstr);}// 設置分離后,對分離的子線程進行連接 pthread_join()// ret = pthread_join(tid, NULL);// if(ret != 0) {// char * errstr = strerror(ret);// printf("error3 : %s\n", errstr);// }pthread_exit(NULL);return 0;
}
2.5 patch_cancel
pthread_cancel 是一個用于取消線程的函數。它的功能是讓線程終止運行,但需要注意的是,這并不意味著線程會立即終止。線程的終止需要等到它執行到一個“取消點”時才會發生。所謂“取消點”,可以簡單理解為系統規定的一些系統調用,這些調用通常涉及從用戶空間到內核空間的切換。當線程執行到這些取消點時,它才會真正終止。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());for(int i = 0; i < 5; i++) {printf("child : %d\n", i);}return NULL;
}int main() {// 創建一個子線程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 取消線程pthread_cancel(tid);for(int i = 0; i < 5; i++) {printf("%d\n", i);}// 輸出主線程和子線程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());pthread_exit(NULL);return 0;
}
2.6 pthread_attr
int pthread_attr_init(pthread_attr_t *attr);- 初始化線程屬性變量int pthread_attr_destroy(pthread_attr_t *attr);- 釋放線程屬性的資源int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);- 獲取線程分離的狀態屬性int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);- 設置線程分離的狀態屬性
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());return NULL;
}int main() {// 創建一個線程屬性變量pthread_attr_t attr;// 初始化屬性變量pthread_attr_init(&attr);// 設置屬性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// 創建一個子線程pthread_t tid;int ret = pthread_create(&tid, &attr, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 獲取線程的棧的大小size_t size;pthread_attr_getstacksize(&attr, &size);printf("thread stack size : %ld\n", size);// 輸出主線程和子線程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());// 釋放線程屬性資源pthread_attr_destroy(&attr);pthread_exit(NULL);return 0;
}
三、實戰demo
下面程序也存在一定的問題,就是可能多個線程賣同一張票,也可能最后會出現賣0張、賣第-1張票的情況出現。這就是線程同步的問題。
/*使用多線程實現買票的案例。有3個窗口,一共是100張票。
*/#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 全局變量,所有的線程都共享這一份資源。
// 如果放到sellticket中,俺么大家都會賣100張,一共賣300張。
int tickets = 100;void * sellticket(void * arg) {// 賣票while(tickets > 0) {usleep(6000);printf("%ld 正在賣第 %d 張門票\n", pthread_self(), tickets);tickets--;}return NULL;
}int main() {// 創建3個子線程pthread_t tid1, tid2, tid3;pthread_create(&tid1, NULL, sellticket, NULL);pthread_create(&tid2, NULL, sellticket, NULL);pthread_create(&tid3, NULL, sellticket, NULL);// 回收子線程的資源,阻塞// pthread_join(tid1, NULL);// pthread_join(tid2, NULL);// pthread_join(tid3, NULL);// 設置線程分離。pthread_detach(tid1);pthread_detach(tid2);pthread_detach(tid3);pthread_exit(NULL); // 退出主線程return 0;
}
四、線程同步
從上面的demo可以看出線程同步的情況需要考慮。
線程的主要優勢在于,能夠通過全局變量來共享信息。不過,這種便捷的共享是有代價的,必須確保多個線程不會同時修改同一變量,或者某一線程不會讀取正在由其他線程修改的變量。
臨界區是指訪問某一共享資源的代碼片段,并且這段代碼的執行應為原子操作,也就是同時訪問同一共享資源的其他線程不應終端該片段的執行。
線程同步:即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作,其他線程才能對該內存地址進行操作,而其他線程則處于等待狀態。
為避免線程更新共享變量時出現問題,可以使用互斥量(mutex是 mutual exclusion的縮寫)來確保同時僅有一個線程可以訪問某項共享資源。可以使用互斥量來保證對任意共享資源的原子訪問。
互斥量有兩種狀態:已鎖定(locked)和未鎖定(unlocked)。任何時候,至多只有一個線程可以鎖定該互斥量。試圖對已經鎖定的某一互斥量再次加鎖,將可能阻塞線程或者報錯失敗,具體取決于加鎖時使用的方法。
一旦線程鎖定互斥量,隨即成為該互斥量的所有者,只有所有者才能給互斥量解鎖。一般情況下,對每一共享資源(可能由多個相關變量組成)會使用不同的互斥量,每一線程在訪問同一資源時將采用如下協議:首先針對共享資源鎖定互斥量,然后訪問共享資源,最后對互斥量解鎖
即可。
簡單來說就是如果多個線程相同時執行一個代碼(臨界區),只有一個線程能夠持有互斥量,其他的線程將阻塞,這樣也就保證了只有一個線程能夠進入這段代碼區域。
先來看看mutex的相關函數
互斥量的類型 pthread_mutex_tint pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);- 初始化互斥量- 參數 :- mutex : 需要初始化的互斥量變量- attr : 互斥量相關的屬性,NULL- restrict : C語言的修飾符,被修飾的指針,不能由另外的一個指針進行操作。pthread_mutex_t *restrict mutex = xxx;pthread_mutex_t * mutex1 = mutex;int pthread_mutex_destroy(pthread_mutex_t *mutex);- 釋放互斥量的資源int pthread_mutex_lock(pthread_mutex_t *mutex);- 加鎖,阻塞的,如果有一個線程加鎖了,那么其他的線程只能阻塞等待int pthread_mutex_trylock(pthread_mutex_t *mutex);- 嘗試加鎖,如果加鎖失敗,不會阻塞,會直接返回。int pthread_mutex_unlock(pthread_mutex_t *mutex);- 解鎖
所以剛剛的代碼可以優化成下面加鎖的形式。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 全局變量,所有的線程都共享這一份資源。
int tickets = 1000;// 創建一個互斥量
pthread_mutex_t mutex;void * sellticket(void * arg) {// 賣票while(1) {// 加鎖pthread_mutex_lock(&mutex);if(tickets > 0) {usleep(6000);printf("%ld 正在賣第 %d 張門票\n", pthread_self(), tickets);tickets--;}else {// 解鎖pthread_mutex_unlock(&mutex);break;}// 解鎖pthread_mutex_unlock(&mutex);}return NULL;
}int main() {// 初始化互斥量pthread_mutex_init(&mutex, NULL);// 創建3個子線程pthread_t tid1, tid2, tid3;pthread_create(&tid1, NULL, sellticket, NULL);pthread_create(&tid2, NULL, sellticket, NULL);pthread_create(&tid3, NULL, sellticket, NULL);// 回收子線程的資源,阻塞pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_join(tid3, NULL);pthread_exit(NULL); // 退出主線程// 釋放互斥量資源pthread_mutex_destroy(&mutex);return 0;
}
五、死鎖
有時一個線程需要同時訪問兩個或更多不同的共享資源,而每個資源又都由不同的互斥量管理。當超過一個線程加鎖同一組互斥量時,就有可能發生死鎖。
兩個或兩個以上的進程在執行過程中,因爭奪共享資源而造成的一種互相等待的現象,若無外力作用它們都將無法推進下去。此時稱系統處于死鎖狀態或系統產生了死鎖。
死鎖的幾種場景:忘記釋放鎖、重復加鎖(也就是一條加鎖的語句連續有兩行這種,會導致第二行就卡住了)、多線程多鎖導致鎖搶占資源。
比如下面這種情況,就是最常見的死鎖情況示意圖。
六、讀寫鎖
當有一個線程已經持有互斥鎖時,互斥鎖將所有試圖進入臨界區的線程都阻塞住。但是考慮一種情形,當前持有互斥鎖的線程只是要讀訪問共享資源,而同時有其它幾個線程也想讀取這個共享資源,但是由于互斥鎖的排它性,所有其它線程都無法獲取鎖,也就無法讀訪問共享資源了,但是實際上多個線程同時讀訪問共享資源并不會導致問題。
在對數據的讀寫操作中,更多的是讀操作,寫操作較少,例如對數據庫數據的讀寫應用。為了滿足當前能夠允許多個讀出,但只允許一個寫入的需求,線程提供了讀寫鎖來實現。
讀寫鎖的特點:
如果有其它線程讀數據,則允許其它線程執行讀操作,但不允許寫操作。
如果有其它線程寫數據,則其它線程都不允許讀、寫操作。
寫是獨占的,寫的優先級高。
讀寫鎖的類型 pthread_rwlock_t
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
/*案例:8個線程操作同一個全局變量。3個線程不定時寫這個全局變量,5個線程不定時的讀這個全局變量
*/#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 創建一個共享數據
int num = 1;
// pthread_mutex_t mutex;
pthread_rwlock_t rwlock;void * writeNum(void * arg) {while(1) {pthread_rwlock_wrlock(&rwlock);num++;printf("++write, tid : %ld, num : %d\n", pthread_self(), num);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;
}void * readNum(void * arg) {while(1) {pthread_rwlock_rdlock(&rwlock);printf("===read, tid : %ld, num : %d\n", pthread_self(), num);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;
}int main() {pthread_rwlock_init(&rwlock, NULL);// 創建3個寫線程,5個讀線程pthread_t wtids[3], rtids[5];for(int i = 0; i < 3; i++) {pthread_create(&wtids[i], NULL, writeNum, NULL);}for(int i = 0; i < 5; i++) {pthread_create(&rtids[i], NULL, readNum, NULL);}// 設置線程分離for(int i = 0; i < 3; i++) {pthread_detach(wtids[i]);}for(int i = 0; i < 5; i++) {pthread_detach(rtids[i]);}getchar();pthread_rwlock_destroy(&rwlock);pthread_exit(NULL);return 0;
}
運行之后可以看到num是非常有順序的。
七、生產消費者模型
下面是一個粗略簡單的生產者消費者demo模型。
但是下面的代碼有一點問題,就是如果沒有數據消費者還是一直while循環,那么就不太好,浪費了資源,所以就引入 條件變量
。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>// 創建一個互斥量
pthread_mutex_t mutex;struct Node{int num;struct Node *next;
};// 頭結點
struct Node * head = NULL;void * producer(void * arg) {// 不斷的創建新的節點,添加到鏈表中while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存頭結點的指針struct Node * tmp = head;// 判斷是否有數據if(head != NULL) {// 有數據head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 沒有數據就釋放鎖pthread_mutex_unlock(&mutex);}}return NULL;
}int main() {pthread_mutex_init(&mutex, NULL);// 創建5個生產者線程,和5個消費者線程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);return 0;
}
條件變量的類型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
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);- 等待多長時間,調用了這個函數,線程會阻塞,直到指定的時間結束。
int pthread_cond_signal(pthread_cond_t *cond);- 喚醒一個或者多個等待的線程
int pthread_cond_broadcast(pthread_cond_t *cond);- 喚醒所有的等待的線程
下面是對上面demo引入條件變量的優化。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>// 創建一個互斥量
pthread_mutex_t mutex;
// 創建條件變量
pthread_cond_t cond;struct Node{int num;struct Node *next;
};// 頭結點
struct Node * head = NULL;void * producer(void * arg) {// 不斷的創建新的節點,添加到鏈表中while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());// 只要生產了一個,就通知消費者消費pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存頭結點的指針struct Node * tmp = head;// 判斷是否有數據if(head != NULL) {// 有數據head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 沒有數據,需要等待// 當這個函數調用阻塞的時候,會對互斥鎖進行解鎖,這樣其他的生產者就可以繼續生產,不會造成死鎖。當不阻塞的時候,繼續向下執行,會重新加鎖。所以這也就是為什么后面還是要繼續多一行代碼進行解鎖。pthread_cond_wait(&cond, &mutex);pthread_mutex_unlock(&mutex);}}return NULL;
}int main() {pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);// 創建5個生產者線程,和5個消費者線程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);pthread_exit(NULL);return 0;
}
接著我們來看看信號量(用于阻塞線程),但是不能保證線程的數據安全問題,如果要保證數據安全問題,需要跟互斥鎖一起使用。
信號量的類型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
信號量的類型 sem_tint sem_init(sem_t *sem, int pshared, unsigned int value);- 初始化信號量- 參數:- sem : 信號量變量的地址- pshared : 0 用在線程間 ,非0 用在進程間- value : 信號量中的值int sem_destroy(sem_t *sem);- 釋放資源int sem_wait(sem_t *sem);- 對信號量加鎖,調用一次對信號量的值-1,如果值為0,就阻塞int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);int sem_post(sem_t *sem);- 對信號量解鎖,調用一次對信號量的值+1int sem_getvalue(sem_t *sem, int *sval);sem_t psem;sem_t csem;init(psem, 0, 8);init(csem, 0, 0);producer() {sem_wait(&psem);sem_post(&csem)}customer() {sem_wait(&csem);sem_post(&psem)}
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>// 創建一個互斥量
pthread_mutex_t mutex;
// 創建兩個信號量
sem_t psem;
sem_t csem;struct Node{int num;struct Node *next;
};// 頭結點
struct Node * head = NULL;void * producer(void * arg) {// 不斷的創建新的節點,添加到鏈表中while(1) {sem_wait(&psem);pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);sem_post(&csem);}return NULL;
}void * customer(void * arg) {while(1) {sem_wait(&csem);pthread_mutex_lock(&mutex);// 保存頭結點的指針struct Node * tmp = head;head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);sem_post(&psem);}return NULL;
}int main() {pthread_mutex_init(&mutex, NULL);sem_init(&psem, 0, 8);sem_init(&csem, 0, 0);// 創建5個生產者線程,和5個消費者線程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);return 0;
}