1. 程式人生 > 實用技巧 >Linux下C語言多執行緒程式設計

Linux下C語言多執行緒程式設計

一、多執行緒基本概念

多執行緒(multithreading),是指從軟體或者硬體上實現多個執行緒併發執行的技術。具有多執行緒能力的計算機因有硬體支援而能夠在同一時間執行多於一個執行緒,進而提升整體處理效能。具有這種能力的系統包括對稱多處理機、多核心處理器以及晶片級多處理或同時多執行緒處理器。在一個程式中,這些獨立執行的程式片段叫作“執行緒”(Thread),利用它程式設計的概念就叫作“多執行緒處理”.(百度)在單核CPU單執行緒的處理器上,對於多執行緒的處理方式,只能分時切換執行緒,每一個執行緒執行一個時間片然後被換出,在這種情況下,無須擔心公共臨界區的變數的競爭問題,相反在對核心CPU中就需要非常嚴格的關注臨界區的資料競爭情況。如下圖所示分別為單核心多核心的執行緒排程情況:

二、多執行緒基本API介紹

1. 在Linux環境下多執行緒程式設計標頭檔案

1 #include <errno.h> // Error code head file(EBUSY,ETIMEDOUT)
2 #include <pthread.h> // Pthread head file

2. 基本執行緒相關函式

1. pthread_mutex_t g_mutex; // 臨界區鎖定義
2. pthread_mutex_init(g_mutex,NULL); // 鎖初始化函式
3. pthread_cond_t g_cond; // 觸發條件定義
4. pthread_cond_init
(g_cond,NULL); // 初始化條件變數 5. int ret = pthread_mutex_lock(&g_mutex); // 獲取鎖,獲取失敗則阻塞在此函式直至獲取到鎖,失敗返回錯誤程式碼EBUSY 6. pthread_mutex_unlock(&g_mutex); // 進行解鎖,釋放鎖 7. ret = pthread_mutex_trylock(&g_mutex); // 嘗試獲取鎖,若獲取失敗,則不阻塞直接跳過,返回值為鎖繁忙EBUSY 8. ret = pthread_mutex_timedlock(&g_mutex ,&outtime); //
嘗試在outtime時間段之內獲取鎖,若超時未獲取則不阻塞,並返回ETIMEDOUT 10. pthread_cond_wait(g_cond,g_mutex); // 先對鎖進行解鎖,並且阻塞等待cond訊號觸發直至觸發完成 11. ret = pthread_cond_timedwait(g_cond,g_mutex,&outtime); // 先對鎖進行解鎖,並且等待cond訊號觸發,若在截止時間之前未觸發,則跳出函式,並返回ETIMEDOUT 12. pthread_cond_signal(g_cond); // 單一發出觸發訊號,觸發等待佇列中的第一個執行緒,假設您只對佇列添加了一個工作作業。那麼只需要喚醒一個工作程式執行緒(再喚醒其它執行緒是不禮貌的!) 13. pthread_cond_broadcast(g_cond); // 發出廣播觸發訊號,通知喚醒等待佇列中的所有執行緒 14. pthread_cond_destroy(g_cond); // 銷燬條件變數,歸還條件變數資源 15. pthread_t th1; // 定義執行緒物件,類似於程序的PID號 16. pthread_create(&th1,NULL,thread_func,NULL); // Create the Thread1 & Start the thread func. 17. pthread_join(th1,NULL); // Wait the Thread1 end. 18. 待補充!

如上所示為基本的多執行緒程式設計API,上述鎖的API的實現包括了多種方式,基本都能夠滿足常見的鎖需求,對應的每一個函式呼叫基本上都有int型別的返回值,一般返回的內容都包括在了error.h標頭檔案中。

其中函式pthread_cond_waitpthread_mutex_lock會阻塞執行緒直至獲取到對應條件,而其他鎖相關函式不會一直阻塞執行緒,從而能夠滿足其他的場景。

3. 阻塞時間outtime引數設定:

\* 如下是timespec結構體的具體型別,其中都是long型 *\
struct timespec
{
  __time_t tv_sec;    /* Seconds.  */
  __syscall_slong_t tv_nsec;    /* Nanoseconds.  */
}; 

timespec結構體可以精確到納秒級別,包括了兩個成員變數,分別為秒tv_sec以及納秒tv_nsec;

具體在延遲阻塞中應用是,可以先獲取系統當前時間,然後在當前時間的基礎上增加執行緒阻塞延遲增量,然後通過指標形參傳遞給對應的執行緒阻塞函式即可,詳參見下:

 1 #include <time.h> // 需要包含時間標頭檔案
 2 long wt_ms; // 需要延遲阻塞的時間
 3 struct timespec outtime; // Defination定義
 4 clock_gettime(CLOCK_REALTIME, &outtime); // Get the current time.(獲取當前系統時間,注意一定要用CLOCK_REALTIME來獲取系統時間)
 5 
 6 /****************************************
 7 *** 開始設定阻塞延遲時間點,在下個時間點觸發 ***
 8 ****************************************/
 9 
10 outtime.tv_sec += wt_ms / 1000; // 毫秒換算秒
11 time_ns = outtime.tv_nsec + (wt_ms % 1000) * 1000000; // 多出的換算成納秒
12 if(time_ns >= 1000000000){ // 溢位當前時間則需要判斷是否進位
13     outtime.tv_sec++; // 進位
14     outtime.tv_nsec = time_ns - 1000000000; // 計算進位後餘
15 }else{
16     outtime.tv_nsec = time_ns; // 無需進位直接賦值即可
17 }
18 ret = pthread_mutex_timedlock(&g_mutex ,&outtime); // 阻塞延遲一段時間,然後返回相關程式碼給ret.

上述程式碼將outtime引數阻塞延遲時間傳遞給了pthread_mutex_timedlock函式:

(a) 若在outtime時間節點之前獲取到鎖,則停止阻塞,返回ret=0;

(b) 若時間節點之後還未獲取鎖,則停止阻塞,返回ret=ETIMEDOUT超時。

4. 自旋鎖

?待補充?

5. 讀寫鎖(非常適合於讀寫執行緒的變數共享情況)

假如現在一個執行緒a只是想讀一個共享變數i,因為不確定是否會有執行緒去寫他,所以我們還是要對它進行加鎖。

但是這時候又一個執行緒b試圖讀共享變數i ,於是發現被鎖住,那麼b不得不等到a釋放了鎖後才能獲得鎖並讀取 i 的值,但是兩個讀取操作即使是幾乎同時發生也並不會像寫操作那樣造成競爭,因為他們不修改變數的值。

所以我們期望如果是多個執行緒試圖讀取共享變數值的話,那麼他們應該可以立刻獲取而不需要等待前一個執行緒釋放因為讀而加的鎖。

讀寫鎖可以很好的解決上面的問題。他提供了比互斥量跟好的並行性。因為以讀模式加鎖後當又有多個執行緒僅僅是試圖再以讀模式加鎖時,並不會造成這些執行緒阻塞在等待鎖的釋放上。

相關讀寫鎖的API具體如下:

1 int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); // 初始化讀寫鎖
2 int pthread_rwlockattr_destory(pthread_rwlockattr_t *attr); // 銷燬回收讀寫鎖
3 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 阻塞方式獲取讀鎖
4 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 阻塞方式獲取寫鎖
5 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 非阻塞方式獲取讀鎖
6 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 非阻塞方式獲取寫鎖
7 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 釋放讀寫鎖

具體使用例項程式碼如下:

 1 #include <stdio.h>
 2 #include <errno.h> // Error code head file(EBUSY)
 3 #include <stdlib.h>
 4 #include <unistd.h>
 5 #include <pthread.h> // Pthread head file
 6 
 7 pthread_rwlock_t rw_lock = PTHREAD_RWLOCK_INITIALIZER;
 8 int global = 0;
 9 
10 void *thread_read_func(void *arg);
11 void *thread_write_func(void *arg);
12 
13 int main(void)
14 {
15     pthread_t th1,th2;
16     printf("Mutil_Thread_Sys Starting...\n"); // Show the Starting point.
17     pthread_mutex_unlock(GMemory->g_mutex);
18     pthread_create(&th2,NULL,thread_read_func,"Read-TH"); // Create the Thread2 & Start the thread func2.
19     pthread_create(&th1,NULL,thread_write_func,"Write_TH"); // Create the Thread1 & Start the thread func1.
20 
21     while(1){ // Main Processing thread.
22         ;
23     }
24 
25     pthread_join(th2,NULL); // wait the Thread2 end.
26     pthread_join(th1,NULL); // Wait the Thread1 end.
27 
28     printf("System Exit.\n"); // Show the Ending point.
29     return 0;
30 }
31 
32 void *thread_read_func(void *arg)
33 {  
34     bool ret;
35     char *pthr_name = (char *)arg;
36     while(1){
37         ret = pthread_rwlock_rdlock(&rw_lock);
38         printf("The %s Read value:%d\n",pthr_name,global);
39         usleep(5000);
40         pthread_rwlock_unlock(&rw_lock);
41     }
42 }
43 
44 void *thread_write_func(void *arg)
45 {
46     bool ret;
47     char *pthr_name = (char *)arg;
48     while(1){
49         ret = pthread_rwlock_wrlock(&rw_lock);
50         global++;
51         usleep(5000);
52         printf("The %s Write thread value:%d\n",pthr_name,global);
53         pthread_rwlock_unlock(&rw_lock);
54     }
55 }
View Code

執行結果如下:

三、執行緒間通訊方式

1. 全域性變數通訊

由於執行緒使用的棧空間和堆空間都是程序的,而多執行緒都屬於程序,故而全域性變數能夠被多個執行緒同時訪問(為了防止使用混亂,採用鎖機制來對全域性變數進行訪問即可);

1 typedef struct Global_Memory{ // #define new struct type var with mutex lock and data also using-time.
2     pthread_mutex_t *g_mutex; // The mutex lock variable define
3     unsigned int Memory[10]; // The truly Data you will deal with.
4 }GMem;

設計如上所示的結構體變數,並在全域性定義,此時即可將Memory空間和g_mutex鎖變數進行了繫結,只有獲取鎖的狀態下才可以修改Memory當中的內容,從而能夠有序的完成執行緒間同信。

2. 訊息佇列

?待補充?

3. 訊號量喚醒

訊號量作為基本的通訊方式,線上程阻塞睡眠喚醒的過程當中,充當著非常重要的角色,如下所示為條件訊號量的定義及使用:

1 pthread_cond_t *g_cond; // condition semaphore define
2 ret = pthread_cond_timedwait(g_cond,GMemory->g_mutex,&outtime); // Waitting for the trigger signal and active the threads in queue while the outtime.
3 ret = pthread_cond_wait(g_cond, GMemory->g_mutex); // unlock the g_mutex lock and wait for the condition trigger.
4 pthread_cond_broadcast(g_cond); // broadcast trigger all threads in the queue.
5 pthread_cond_signal(g_cond); // Only wake up the first thread in the front of wait-queue.

2,3行線上程中用來等待其他執行緒使用4,5行來進行cond訊號量喚醒。

四、生產者消費者模型

1. 如下圖所示為基本的執行緒狀態切換流程:

2. 基本相關多執行緒框架案例

(a) 延遲阻塞多執行緒框架:

框架介紹:多個執行緒在不同時間節點啟動,獲取同一公共資源,每個執行緒都能設定自身獨立的阻塞等待的時間,若獲取資源等待超時,則放棄等待。

 1 bool th_queue_run(long wt_ms, long choke_ms,int th)
 2 {
 3     int ret;
 4     long time_ns=0;
 5     struct timespec outtime;
 6     clock_gettime(CLOCK_REALTIME, &outtime); // Get the current time.
 7     outtime.tv_sec += wt_ms / 1000;
 8     time_ns = outtime.tv_nsec + (wt_ms % 1000) * 1000000;
 9     if(time_ns >= 1000000000){
10         outtime.tv_sec++;
11         outtime.tv_nsec = time_ns - 1000000000;
12     }else{
13         outtime.tv_nsec = time_ns;
14     }
15     ret = pthread_mutex_timedlock(GMemory->g_mutex ,&outtime);
16     if(ret != 0){
17         if(ret == ETIMEDOUT){
18             printf("%d Timeout | Compete, Running Failed!\n",th);
19             return false;
20         }
21     }
22     printf("%d Running successfully.\n",th);
23     GMemory->Memory[th]++;
24     printf("Memory[3] Memory[4] **** %d %d\n",GMemory->Memory[3],GMemory->Memory[4]);
25     sleep(choke_ms);
26     pthread_mutex_unlock(GMemory->g_mutex);
27     return true;
28 }

(b) 未完待續

Reference:

樹莓派多執行緒點燈:https://github.com/embedded-learning-group/Linux_Learning/tree/master/20190818_Lesson7

相關thread API函式介面介紹:https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutex_timedlock.html

POSIX執行緒詳解(第三部分):https://www.ibm.com/developerworks/cn/linux/thread/posix_thread3/index.html

執行緒同步及訊息佇列:https://www.cnblogs.com/noticeable/p/8549788.html