【C++高并發服務器WebServer】-9:多線程開發

在這里插入圖片描述

本文目錄

  • 一、線程概述
    • 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;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/67321.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/67321.shtml
英文地址,請注明出處:http://en.pswp.cn/web/67321.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

14-6-1C++STL的list

(一&#xff09;list容器的基本概念 list容器簡介&#xff1a; 1.list是一個雙向鏈表容器&#xff0c;可高效地進行插入刪除元素 2.list不可以隨機存取元素&#xff0c;所以不支持at.(pos)函數與[ ]操作符 &#xff08;二&#xff09;list容器頭部和尾部的操作 list對象的默…

在sortablejs的拖拽排序情況下阻止input拖拽事件

如題 問題 在vue3的elementPlus的table中&#xff0c;通過sortablejs添加了行拖拽功能&#xff0c;但是在行內會有輸入框&#xff0c;此時拖拽輸入框會觸發sortablejs的拖拽功能 解決 基于這個現象&#xff0c;我懷疑是由于拖拽事件未綁定而冒泡到后面的行上從而導致的拖拽…

21.Word:小趙-畢業論文排版?【39】

目錄 題目? NO1.2 NO3.4 NO5.6 NO7.8.9 NO10.11.12 題目 NO1.2 自己的論文當中接收老師的修改&#xff1a;審閱→比較→源文檔&#xff1a;考生文件夾&#xff1a;Word.docx→修訂的文檔&#xff1a;考生文件夾&#xff1a;教師修改→確定→接收→接收所有修訂將合并之…

leetcode_鏈表 876.鏈表的中間節點

876.鏈表的中間節點 給你單鏈表的頭結點 head &#xff0c;請你找出并返回鏈表的中間結點。如果有兩個中間結點&#xff0c;則返回第二個中間結點。思路&#xff1a;快慢指針&#xff0c;創建兩個指針fast和slow&#xff0c;fast指針每次移動兩步&#xff0c;slow指針每次移動…

深度學習 DAY3:NLP發展史及早期的前饋神經網絡(ANN)及多任務學習

NLP發展史 NLP發展脈絡簡要梳理如下&#xff1a; 2001 - Neural language models&#xff08;神經語言模型&#xff09; 2008 - Multi-task learning&#xff08;多任務學習&#xff09; 2013 - Word embeddings&#xff08;詞嵌入&#xff09; 2013 - Neural networks for NL…

全面了解 Web3 AIGC 和 AI Agent 的創新先鋒 MelodAI

不管是在傳統領域還是 Crypto&#xff0c;AI 都是公認的最有前景的賽道。隨著數字內容需求的爆炸式增長和技術的快速迭代&#xff0c;Web3 AIGC&#xff08;AI生成內容&#xff09;和 AI Agent&#xff08;人工智能代理&#xff09;正成為兩大關鍵賽道。 AIGC 通過 AI 技術生成…

54.數字翻譯成字符串的可能性|Marscode AI刷題

1.題目 問題描述 小M獲得了一個任務&#xff0c;需要將數字翻譯成字符串。翻譯規則是&#xff1a;0對應"a"&#xff0c;1對應"b"&#xff0c;依此類推直到25對應"z"。一個數字可能有多種翻譯方法。小M需要一個程序來計算一個數字有多少種不同的…

FileReader使用

FileReader : 讀取文件內容的api&#xff0c;&#xff0c;&#xff0c;在前端處理上傳的文件&#xff0c;&#xff0c;比如預覽圖片 readAsDataURL(file) &#xff1a; 讀取為base64編碼的 data urlreadAsText() &#xff1a; 讀取為文本readAsArrayBuffer() : 讀取為二進制 …

RabbitMQ5-死信隊列

目錄 死信的概念 死信的來源 死信實戰 死信之TTl 死信之最大長度 死信之消息被拒 死信的概念 死信&#xff0c;顧名思義就是無法被消費的消息&#xff0c;一般來說&#xff0c;producer 將消息投遞到 broker 或直接到queue 里了&#xff0c;consumer 從 queue 取出消息進…

JavaScript系列(48)-- 3D渲染引擎實現詳解

JavaScript 3D渲染引擎實現詳解 &#x1f3ae; 今天&#xff0c;讓我們深入探討JavaScript的3D渲染引擎實現。通過WebGL和現代JavaScript技術&#xff0c;我們可以構建一個功能完整的3D渲染系統。 3D渲染基礎概念 &#x1f31f; &#x1f4a1; 小知識&#xff1a;3D渲染引擎的…

10JavaWeb——SpringBootWeb案例01

前面我們已經講解了Web前端開發的基礎知識&#xff0c;也講解了Web后端開發的基礎(HTTP協議、請求響應)&#xff0c;并且也講解了數據庫MySQL&#xff0c;以及通過Mybatis框架如何來完成數據庫的基本操作。 那接下來&#xff0c;我們就通過一個案例&#xff0c;來將前端開發、后…

【面試題】 Java 三年工作經驗(2025)

問題列表 為什么選擇 spring boot 框架&#xff0c;它與 Spring 有什么區別&#xff1f;spring mvc 的執行流程是什么&#xff1f;如何實現 spring 的 IOC 過程&#xff0c;會用到什么技術&#xff1f;spring boot 的自動化配置的原理是什么&#xff1f;如何理解 spring boot 中…

JAVA 接口、抽象類的關系和用處 詳細解析

接口 - Java教程 - 廖雪峰的官方網站 一個 抽象類 如果實現了一個接口&#xff0c;可以只選擇實現接口中的 部分方法&#xff08;所有的方法都要有&#xff0c;可以一部分已經寫具體&#xff0c;另一部分繼續保留抽象&#xff09;&#xff0c;原因在于&#xff1a; 抽象類本身…

ResNeSt: Split-Attention Networks論文學習筆記

這張圖展示了一個名為“Split-Attention”的神經網絡結構&#xff0c;該結構在一個基數組&#xff08;cardinal group&#xff09;內進行操作。基數組通常指的是在神經網絡中處理的一組特征或通道。圖中展示了如何通過一系列操作來實現對輸入特征的注意力機制。 以下是圖中各部…

數據收集后臺服務概要設計

為了幫助大家設計一個數據指標匯總的后端應用&#xff0c;我將提供一個概要設計和表設計的建議。這個設計將基于常見的數據收集需求&#xff0c;假設你需要收集、存儲和匯總來自不同數據源的指標數據。 1. 概要設計 1.1 系統架構 數據收集層&#xff1a;負責從不同數據源&am…

探秘 TCP TLP:從背景到實現

回家的路上還討論了個關于 TCP TLP 的問題&#xff0c;閑著無事縷一縷。本文內容參考自 Tail Loss Probe (TLP): An Algorithm for Fast Recovery of Tail Losses 以及 Linux 內核源碼。 TLP&#xff0c;先說緣由。自 TCP 引入 Fast retrans 機制就是為了盡力避免 RTO&#xf…

設計模式Python版 原型模式

文章目錄 前言一、原型模式二、原型模式示例三、原型管理器 前言 GOF設計模式分三大類&#xff1a; 創建型模式&#xff1a;關注對象的創建過程&#xff0c;包括單例模式、簡單工廠模式、工廠方法模式、抽象工廠模式、原型模式和建造者模式。結構型模式&#xff1a;關注類和對…

一文大白話講清楚webpack進階——5——dev-server原理及其作用

文章目錄 一文大白話講清楚webpack進階——5——dev-server原理及其作用1. webpack的作用2. dev-server的作用3. dev-server的原理3.1 啥是webpack-dev-middleware3.2 HMR 一文大白話講清楚webpack進階——5——dev-server原理及其作用 1. webpack的作用 webpack的作用我們之…

【第十天】零基礎入門刷題Python-算法篇-數據結構與算法的介紹-兩種常見的字符串算法(持續更新)

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、Python數據結構與算法的詳細介紹1.Python中的常用的字符串算法2.字符串算法3.詳細的字符串算法1&#xff09;KMP算法2&#xff09;Rabin-Karp算法 總結 前言…

Writing an Efficient Vulkan Renderer

本文出自GPU Zen 2。 Vulkan 是一個新的顯式跨平臺圖形 API。它引入了許多新概念&#xff0c;即使是經驗豐富的圖形程序員也可能不熟悉。Vulkan 的主要目標是性能——然而&#xff0c;獲得良好的性能需要深入了解這些概念及其高效應用方法&#xff0c;以及特定驅動程序實現的實…