.
.
.
.
.
目錄
(一) 一起學 Unix 環境高級編程 (APUE) 之 標准IO
(二) 一起學 Unix 環境高級編程 (APUE) 之 文件 IO
(三) 一起學 Unix 環境高級編程 (APUE) 之 文件和目錄
(四) 一起學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息
(五) 一起學 Unix 環境高級編程 (APUE) 之 進程環境
(六) 一起學 Unix 環境高級編程 (APUE) 之 進程控制
(七) 一起學 Unix 環境高級編程 (APUE) 之 進程關系 和 守護進程
(八) 一起學 Unix 環境高級編程 (APUE) 之 信號
(九) 一起學 Unix 環境高級編程 (APUE) 之 線程
(十) 一起學 Unix 環境高級編程 (APUE) 之 線程控制
之前我們在創建線程的時候都是使用的默認屬性,本章主要討論的是自定義線程的屬性。
使用默認屬性基本上能解決掉遇到的大部分問題,所以自定義屬性在實際項目中用得比較少。
1.線程屬性
《APUE》第三版 P341 表中的屬性可以用來限定一個進程能創建線程的最大數量,但是限定線程數量的宏不必太當真,因為在上一篇博文中我們說過了一個線程能創建的線程的數量是受很多因素影響的,並非一定是以這幾個宏值為准的。
線程屬性使用 pthread_attr_t 類型表示。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <pthread.h> 4 #include <string.h> 5 6 static void *func(void *p) 7 { 8 puts("Thread is working."); 9 10 pthread_exit(NULL); 11 } 12 13 int main() 14 { 15 pthread_t tid; 16 int err, i; 17 pthread_attr_t attr; 18 19 pthread_attr_init(&attr); 20 // 修改每個線程的棧大小 21 pthread_attr_setstacksize(&attr,1024*1024); 22 23 for(i = 0 ; ; i++) 24 { 25 // 測試當前進程能創建多少個線程 26 err = pthread_create(&tid,&attr,func,NULL); 27 if(err) 28 { 29 fprintf(stderr,"pthread_create():%s\n",strerror(err)); 30 break; 31 } 32 33 } 34 35 printf("i = %d\n",i); 36 37 pthread_attr_destroy(&attr); 38 39 exit(0); 40 }
上面的栗子就是通過線程的屬性修改了為每個線程分配的棧空間大小,這樣創建出來的線程數量與默認的就不同了。
線程屬性使用 pthread_attr_init(3) 函數初始化,用完之後使用 pthread_attr_destroy(3) 函數銷毀。
線程屬性不僅可以設定線程的棧空間大小,還可以創建分離的線程等等。
2.互斥量屬性
互斥量屬性使用 pthread_mutexattr_t 類型表示,與線程屬性一樣,使用之前要初始化,使用完畢要銷毀。
pthread_mutexattr_init(3) 函數用於初始化互斥量的屬性,用法跟線程的屬性很相似。
1 pthread_mutexattr_getpshared, pthread_mutexattr_setpshared - get and 2 set the process-shared attribute 3 4 #include <pthread.h> 5 6 int pthread_mutexattr_getpshared(const pthread_mutexattr_t * 7 restrict attr, int *restrict pshared); 8 9 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, 10 int pshared);
函數名稱裡面的 p 是指 process,這兩個函數的作用是設定線程的屬性是否可以跨進程使用。這條有點亂是吧,線程的屬性怎麼能跨進程使用呢?別急,我們先看看 clone(2) 函數。
1 clone, __clone2 - create a child process 2 3 #define _GNU_SOURCE 4 #include <sched.h> 5 6 int clone(int (*fn)(void *), void *child_stack, 7 int flags, void *arg, ... 8 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
clone(2) 進程的 flags 如果設置了 CLONE_FILES 則父子進程共享文件描述符表,正常情況文件描述符表是線程之間共享的,因為多線程是運行在同一個進程的地址空間之內的。
雖然 clone(2) 函數的描述是創建子進程,但實際上如果將 flags 屬性設置得極端分離(各種資源都獨享),相當於創建了一個子進程;
而如果 flags 屬性設置得極端近似(各種資源都共享),則相當於創建了兄弟線程。所以對於內核來講並沒有進程這個概念,只有線程的概念。你創建出來的到底是進程還是線程,並不影響內核進行調度。
如果需要創建一個“東西”與當前的線程既共享一部分資源,又獨占一部分資源,就可以使用 clone(2) 函數創建一個既不是線程也不是進程的“東西”,因為對內核來說進程和線程本來就是模糊的概念。
現在能理解為什麼上面說 pthread_mutexattr_setpshared(3) 函數的作用是設定線程的屬性是否可以跨進程使用了吧?
互斥量分為四種,不同的互斥量在遇到不同的情況時效果是不同的,《APUE》第三版 P347 有圖12-5 說明了這個現象,LZ 把它照搬到這裡。
互斥量類型 沒有解鎖時重新加鎖 不占用時解鎖 在已解鎖時解鎖 PTHREAD_MUTEX_NORMAL(常規) 死鎖 未定義 未定義 PTHREAD_MUTEX_ERRORCHECK(檢錯) 返回錯誤 返回錯誤 返回錯誤 PTHREAD_MUTEX_RECURSIVE(遞歸) 允許 返回錯誤 返回錯誤PTHREAD_MUTEX_DEFAULT
(默認,我們平時使用的就是這個)
未定義 未定義 未定義表1 互斥量類型行為
LZ 解釋一下表頭上的描述是什麼意思:
1)沒有解鎖時重新加鎖:當前 mutex 已 lock,再次 lock 的情況;
2)不占用時解鎖:他人鎖定由你解鎖的情況;
3)在已解鎖時解鎖:當前 mutex 已 unlock,再次 unlock 的情況;
3.重入
第一次見到重入是在信號階段是吧。
如果一個函數在相同的時間點可以被多個線程安全地調用,就稱該函數是線程安全的。
POSIX 標准要求,在線程標准制定之後,所有的庫必須支持線程安全,如果不支持線程安全需要在函數名添加 _unlocked 後綴,或發布一個支持線程安全的函數,函數名要添加 _r 後綴。
我們在 man 手冊中已經見過很多帶有 _r 後綴的函數了。
4.線程特定數據
就是為了某些數據支持多線程並發而做的改進。最典型的就是 errno,errno 最初是全局變量,現在早已變成宏定義了。
我們把 errno 預編譯一下,看看它的廬山真面目。
1 #include <errno.h> 2 3 errno;
1 >$ gcc -E errno.c 2 # 2 "errno.c" 2 3 4 (*__errno_location ()); 5 >$
5.線程的取消
在上一篇博文中我們說過,pthread_cancel(3) 函數只是提出取消請求,並不能強制取消線程。
線程的取消分為兩種情況:允許取消 或 不允許取消。
pthread_cancel(3) 提出取消請求後,是否允許取消是由被請求取消的線程自己決定的。
不允許取消沒什麼好說的,我們說說允許取消。
允許取消分為兩種情況:異步 cancel 和 推遲 cancel(默認)
1)異步 cancel:是內核的操作方式,這裡不做解釋。
2)推遲 cancel:推遲到取消點再響應取消操作。取消點其實就是一個函數,收到取消請求時取消點的代碼不會執行。
《APUE》第三版 P362 圖12-14 都是可能導致阻塞的系統調用,它們都是 POSIX 定義的一定存在的取消點。P363 圖12-15 是 POSIX 定義的可選取消點,這些函數實際是否為取消點要看平台具體的實現。
為什麼要采用推遲取消的策略,而不是收到請求在任何地方都立即取消呢?我們先舉個栗子說明這個問題,大家請看下面的偽代碼:
1 thr_func() 2 3 { 4 5 p = malloc(); 6 7 -------------------------->收到了一個取消請求 8 9 -------------------------->pthread_cleanup_push();->free(p); // 不是取消點,繼續執行 10 11 fd1 = open(); // 是取消點,在取消點執行之前響應取消動作 12 13 -------------------------->pthread_cleanup_push();->close(fd1); 14 15 fd2 = open(); 16 17 -------------------------->pthread_cleanup_push();->close(fd2); 18 19 pthread_exit(); 20 21 }
在線程執行函數運行的任何時候都可能收到取消請求,假設上面的函數剛剛使用 malloc(3) 函數動態分配了一段內存,還沒來得及掛鉤子函數的時候就收到了一個取消請求,如果立即響應這個取消請求就會導致內存洩漏。而掛載鉤子函數的宏 pthread_cleanup_push 不是取消點,所以會推遲這個取消請求繼續工作。等它把鉤子函數掛載完畢之後繼續運行來到 open(2) 函數,由於 open(2) 函數時有效的取消點,所以響應了這個取消請求,線程被取消並且通過鉤子函數釋放了上面 malloc(3) 所申請的空間。這就是推遲取消最明顯的作用。
pthread_setcancelstate(3) 函數的作用就是修改線程的可取消狀態,可以將線程設置為可取消的或不可取消的。
pthread_setcanceltype(3) 函數用來修改取消類型,也就是可以選擇 異步 cancel 和 推遲 cancel。
pthread_testcancel(3) 函數的作用是人為放置取消點。假如某個線程一啟動就瘋狂的做數學運算10分鐘,沒有調用任何函數,則這個線程無法響應取消,為了使這個線程可以響應取消就可以通過這個函數人為放置取消點。
6.線程和信號
圖1 線程級別的信號位圖
在前面討論信號的博文中,LZ 給大家畫過一張信號處理過程的草圖,在那幅圖中簡單的把一個線程的標准信號畫成了兩個位圖。而實際上每個線程級別都持有一個 mask 位圖和一個 padding 位圖,每個進程級別持有一個 padding 位圖而沒有 mask 位圖。從內核態回到用戶態之前,當前線程先用自己的 mask 位圖與進程級別的 padding 做按位與(&)運算,如果有信號就要去處理;然後再用自己的 mask 位圖與自己的 padding 位圖做按位與運算,再處理相應的信號。
所以其實是哪個線程被調度,就由哪個線程響應進程級別的信號。
由此可見,線程之間也是可以互相發信號的。
1 pthread_kill - send a signal to a thread 2 3 #include <signal.h> 4 5 int pthread_kill(pthread_t thread, int sig); 6 7 Compile and link with -pthread.
pthread_kill(3) 函數的作用就是在線程階段發信號,thread 表示給哪個線程發送信號,sig 是發送哪個信號。
由於這個函數使用起來很簡單,這裡 LZ 就不把栗子貼出來了,大家自己動手寫寫試試吧。
pthread_sigmask(3) 函數的作用時人為的干預線程級別的 mask 位圖。與 sigsetmask(3) 函數很像,大家自己動手試試吧。
7.線程和 fork
這一小節主要說的是 fork(2) 在不同平台上實現有歧義。
在fork的發展過程中主要有兩大陣營,一大陣營使用寫時拷貝技術,另一大陣營使用類似 vfork(2) 的策略。
這兩種策略在前面我們討論進程關系的博文中都討論過,感興趣的童鞋可以自己看看書上的描述,這裡就不做太多的介紹了。
8.線程和 I/O
這一小節主要就是介紹了下 pread(2) 和 pwrite(2) 函數,這兩個函數實際當中用得並不多,感興趣的童鞋自己看看書上的介紹或者看看 man 手冊裡的說明吧,這裡不做過多的討論了。如果有什麼問題可以在評論中留言。
到這裡 POSIX 標准的線程就介紹完了。*nix 平台線程的標准不只有 POSIX 一家,還有像 OpenMP 等標准也定義了不同的線程實現方式。
9.OpenMP 標准
我們使用 OpenMP 標准寫一個 Hello World 程序。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <omp.h> 4 5 int main() 6 { 7 #pragma omp parallel sections 8 { 9 #pragma omp section 10 printf("[%d]:Hello\n",omp_get_thread_num()); 11 #pragma omp section 12 printf("[%d]:World\n",omp_get_thread_num()); 13 } 14 15 exit(0); 16 }
OpenMP 標准的多線程就是使用 # 這種預處理標簽實現的,使用 GCC 編譯的時候需要加 -fopenmp 參數。
>$ make hello cc -fopenmp -Wall hello.c -o hello >$ ./hello [0]:Hello [1]:World >$ ./hello [1]:World [0]:Hello >$
從上面的運行結果可以看出來,線程已經創建,並且已經發生了競爭。
GCC 從 4.0 以上的版本開始支持 OpenMP 標准。
由於 OpenMP 標准不是 《APUE》裡面介紹的,所以我們這裡就不做過多的探討了,感興趣的小伙伴們可以去 http://www.openmp.org 了解更多內容。