《UNIX環境高級編程》第12章 線程控制

12.1 引言

上一章講了線程以及線程同步的基礎知識。
本章將講解控制線程的行爲方面的詳細內容,介紹線程屬性同步原語屬性。前面的章節中使用的都是它們的默認行爲,沒有進行詳細的介紹。
還將介紹同一進程的多個線程之間如何保持數據的私有性。最後討論基於進程的系統調用如何與線程進行交互

12.2 線程限制

SUS定義了線程操作有關的一些限制。於其他的系統限制一樣,這些限制也可以通過sysconf函數進行查詢。

限制名稱 name參數 描述 linux限制
PTHREAD_DESTRUCTOR_ITERATIONS _SC_PTHREAD_DESTRUCTOR_ITERATIONS 線程退出時操作系統實現試圖銷燬線程特定數據的最大次數 4
PTHREAD_KEYS_MAX _SC_PTHREAD_KEYS_MAX 進程可以創建的鍵的最大數目 1024
PTHREAD_STACK_MIN _SC_PTHREAD_STACK_MIN 一個線程的棧可用的最小字節數 16384
PTHREAD_THREADS_MAX _SC_PTHREAD_THREADS_MAX 進程可以創建的最大線程數 沒有確定的限制

12.3 線程屬性

pthread接口允許我們通過設置每個對象關聯的不同屬性細調線程和同步對象的行爲

  1. 每個對象與它自己類型的屬性對象進行關聯。一個屬性對象可以代表多個屬性。
  2. 有一個初始化函數,把屬性設置爲默認值。
  3. 還有一個銷燬屬性對象的函數。如果初始化函數分配了與屬性對象關聯的資源,銷燬函數負責釋放這些資源
  4. 每個屬性都有一個從屬性對象中獲取屬性值的函數。
  5. 每個屬性都一個設置屬性值的函數。

可以使用pthread_attr_init函數初始化pthread_attr_t結構。在調用pthread_attr_init以後,pthread_attr_t結構所包含的就是操作系統實現支持的所有線程屬性的默認值。

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

下面列出了一些常用屬性:

名稱 描述
detachstate 線程的分離狀態屬性
guardsize 線程棧末尾的警戒緩衝區大小(字節數)
stacksddr 線程棧的最低地址
stacksize 線程棧的最小長度(字節數)

11.5節介紹了分離線程的概念。如果對現有的某個線程的終止狀態不感興趣的話,可以使用pthread_detach函數讓操作系統在線程退出時收回它所佔用的資源。
如果在創建線程時就知道不需要了解線程的終止狀態,就可以修改結構中的detachstate屬性,讓線程一開始就處於分離狀態。
可以使用pthread_attr_setdetachstate函數把線程屬性detachstate設置成以下兩個合法值之一:PTHREAD_CREATE_DETACHED以分離方式啓動線程;或者PTHREAD_CREATE_JOINABLE,正常啓動線程,應用程序可以獲取線程的終止狀態。

#include <pthread.h>
int pthread_attr_getdetachstate(pthread_attr_t *restrict attr,int *detachstate);
int pthread_attr_destroy(pthread_attr_t *attr,int *detachstate);

可以調用pthread_attr_getdetachstate函數獲取當前的detachstate線程屬性。


對於POSIX標準的操作系統,並不一定要支持線程棧屬性,但是對於SUS標準的操作系統來說,支持線程棧屬性就是必須的。可以在編譯階段使用_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE符號來檢查系統是否支持每一個線程棧屬性。如果系統定義了這些符號中的一個就說明它支持相應的線程棧屬性。或者也可以在運行階段把_SC_POSIX_THREAD_ATTR_STACKADDR和_SC__POSIX_THREAD_ATTR_STACKSIZE參數傳遞給sysconf函數,檢查運行時系統對線程棧屬性的支持情況。
可以使用函數pthread_attr_getstack和pthread_attr_setstack對線程棧屬性進行管理。

#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *restrict attr,void **restrict stackaddr,size_t *restrict stacksize);
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);

對於進程來說,虛擬地址空間的大小是固定的。因爲進程中只有一個棧,所以它的大小通常不是問題。但對於線程來說,同樣大小的虛擬地址空間必須被所以的線程棧共享。如果應用程序使用了許多線程,以至這些線程棧的累積大小超過了可用的虛擬地址空間,就需要減少默認的線程棧大小。另一方面,如果線程調用的函數分配了大量的自動變量,或者調用的函數涉及許多很深的棧幀(stack frame),那麼需要的棧大小可能比默認的大。
如果線程的虛擬地址空間都用完了,那可以使用malloc或者mmap來爲可替代的棧分配空間,並用*pthread_attr_setstack函數來改變新建線程棧的位置*。


應用程序也可以通過pthread_attr_getstacksizepthread_attr_setstacksize函數讀取或設置線程屬性stacksize

#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);

如果希望改變棧的大小,但又不想自己處理線程棧的分配問題(即線程棧的首地址),這時使用pthread_attr_setstacksiz函數就非常有用。設置stacksize屬性時,選擇stacksize不能小於PTHREAD_STACK_MIN.


線程屬性guardsize控制着線程棧末尾之後用以避免棧溢出的擴展內存大小。這個屬性默認值是由具體實現來定義的,但常用值是系統頁大小。可以把guardsize線程屬性設置爲0,不允許屬性的這種特徵行爲發生:在這種情況下,不會提供警戒緩衝區。同樣,如果修改了線程屬性stackaddr,系統就認爲我們將自己管理棧,進而使棧警戒區機制無效,這等同於把guardsize線程屬性設置爲0

#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);

如果guardsize線程屬性被修改了,操作系統可能會把它取爲頁大小的整數倍。如果線程的棧指針溢出到警戒區域,應用程序就可能通過信號接收到出錯信息。
線程還有一些其他的pthread_attr_t結構中沒有表示的屬性:可撤銷狀態和可撤銷類型。

12.4 同步屬性

就像線程具有屬性一樣,線程的同步對象也有屬性。

12.4.1 互斥量屬性

互斥量屬性是用pthread_mutexattr_t結構表示的。上一章中每次對互斥量進行初始化時,都是通過PTHREAD_MUTEX_INITIALZER常量或使用指向互斥量屬性結構的空指針作爲參數調用pthread_mutex_init函數,得到互斥量的默認屬性。
對於非默認屬性,可以用pthread_mutexattr_init初始化pthread_mutexattr_t結構,用pthread_mutexattr_destory 來反初始化。

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destory (pthread_mutexattr_t *attr);

int pthread_mutexattr_init函數將用默認的互斥量屬性初始化pthread_mutexattr_t結構。值得注意的3個屬性是:

  • 進程共享屬性
  • 健壯屬性
  • 類型屬性

進程共享屬性可以通過符號_POSIX_THREAD_PROCESS_SHARED符號來判斷平臺是否支持這個屬性。也可以通過_SC_POSIX_THREAD_PROCESS_SHARE參數傳遞給sysconf函數進行檢查。
在進程中,多個線程可以訪問同一個同步對象。這時進程共享互斥量屬性設置爲PTHREAD_PROCESS_PRIVATE
在14和15章中將會看到:允許相互獨立的多個進程把同一個內存數據塊映射到它們各自獨立的地址空間中。進程間共享數據也需要同步。這時進程共享互斥量屬性設置爲PTHREAD_PROCESS_SHARED,從多個進程彼此之間共享的內存數據塊中分配的互斥量就可以用於這些進程的同步。

#include <pthread.h>
int pthread_mutexattr_getshared(const pthread_mutexattr_t *restrict attr,int *restrict pshared);
int pthread_mutexattr_setshared(pthread_mutexattr_t *attr,int *pshared);

以上兩函數用於設置和讀取進程互斥量共享屬性


互斥量健壯屬性與在多個進程間共享互斥量有關。
這裏僅瞭解一下,APUE Pg 346.


類型互斥量屬性控制着互斥量的鎖定特性

互斥量類型 含義
PTHREAD_MUTEX_NORMAL 一種標準互斥量類型,不做任何特殊的錯誤檢查或死鎖檢測。
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_DEFAULT
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int *type);

12.4.2讀寫鎖屬性

讀寫鎖與互斥量類似,也是有屬性的。以下是初始化與反初始化屬性結構的函數:

#include <pthread.h>
int pthread_rwlockattr_init(const pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(const pthread_rwlockattr_t *attr);

讀寫鎖唯一支持的屬性是進程共享屬性。它與互斥量的進程共享屬性是相同的。就像互斥量的進程共享屬性一樣,有一對函數用於讀取和設置讀寫鎖的進程共享屬性。

#include <pthread.h>
int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr,int pshared);

雖然POSIX標準只定義了一個讀寫鎖屬性,但不同平臺的實現可以自由地定義額外的、非標準的屬性。

12.4.3條件變量屬性

SUS定義了條件變量的兩個屬性:進程共享屬性時鐘屬性

#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

與其他同步屬性一樣,條件變量支持進程共享屬性。它控制着條件變量是可以被單進程的多個線程使用,還是可以被多進程的線程使用。以下函數用於操作進程共享屬性的值。

#include <pthread.h>
int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr,int pshared);

時鐘屬性控制pthread_cond_timeout函數的超時參數採用的是哪個時鐘。以下函數用於設置和獲取超時時鐘:

#include <pthread.h>
int pthread_condattr_getclock(pthread_condattr_t *attr,clock_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,clock_t clock_id);

12.4.4 屏障屬性

屏障也有屬性。使用以下函數進程初始化和反初始化:

#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);

目前定義的屏障屬性只有進程共享屬性,它控制着屏障是可以被多進程線程使用,還是隻能被初始化屏障的進程內的多線程使用。以下函數用於獲取或設置屏障共享屬性。

#include <pthread.h>
int pthread_barrierattr_getshared(const pthread_barrierattr_t *restrict  attr,int *restrict pshared);
int pthread_barrierattr_setshared(pthread_barrierattr_t *attr,int pshared);

進程共享屬性的值可以是PTHREAD_PROCESS_SHARED(多進程中的多個線程可用),也可以是PTHREAD_PROCESS_PRIVATE(只有初始化屏障的那個進程內的多個線程可以)。

12.5 重入

多個控制線程在相同的時間點有可能調用相同的函數。如果一個函數在相同的時間點可以被多個線程安全地調用,就稱該函數是線程安全的。
如果一個函數多個線程來說是可重入的,就說這個函數就是線程安全的。但這並不能說明對信號處理程序來說該函數也是可重入的。如果函數對異步信號處理程序的重入是安全的,那麼就說函數是異步信號安全的
(異步信號安全和線程安全的區別???)
可能是因爲函數內有加鎖操作。
- 情況1:比如一個函數對資源加鎖了,在調用這個函數時信號發生了,信號處理程序中又試圖對資源加鎖,這時就發生了死鎖。
- 情況2:如果一個線程調用的函數對資源加鎖,此時內核調度了另一個線程,函數對此資源再次加鎖,會阻塞。待到前一個線程解鎖時線程二回獲得該鎖。


POSIX還提供了以線程安全的方式管理FILE對象的方法。可以使用flockfile和ftrylockfile獲取給定FILE對象關聯的鎖。這個鎖是遞歸的:當佔有這把鎖的時候,還是可以再次獲得該鎖,而且不會導致死鎖。

#include <stdio.h>
int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp);

如果標準IO例程都要獲得他們各自的鎖,那麼在做一次一個字符的IO時就會出現嚴重的性能下降。爲了避免這種開銷,出現了不加鎖版本的基於字符的標準IO例程。

#include <stdio.h>
int getchar_unlocked(void);
void getc_unlocked(FILE *fp);

int putchar_unlocked(int c);
int putc_unlocked(int c,FILE *fp);

除非被flockfile或(ftrylockfile)和funlockfile的調用包圍,否則儘量不要調用這4個函數,因爲他們會導致不可預期的結果。
一旦對FILE對象進行加鎖,就可以在釋放鎖之前對這些函數進行多次調用。這樣就可以在多次的數據讀寫上分攤總的加解鎖的開銷。

12.6 線程特定數據

線程特定數據(hread_specific data),也稱線程私有數據(thread_private data),是存儲和查詢某個特定線程相關數據的一種機制。我們把這種數據稱爲線程特定數據或線程私有數據的原因是,我們希望每個線程可以訪問它自己單獨的數據副本,而不需要擔心與其他線程的同步訪問問題
線程模型促進了進程中數據和屬性的共享,那麼爲什麼又需要促進阻止共享的接口呢?這有兩個原因:

  • 第一,有時候需要維護基於每個線程(per-thread)的數據。這些數據是獨立於某個線程的。
  • 第二,它提供了讓基於進程的接口適應多線程環境的機制。一個很明顯的實例就是errno。以前基於進程的接口errno定義爲進程上下文中全局可訪問的整數。系統調用和庫例程在調用或執行失敗時設置errno,把它作爲操作失敗的附屬結果。爲了讓線程也能使用哪些原本基於進程的系統調用和庫例程,errno被重新定義爲線程私有數據。這樣,一個線程做了重置errno的操作也不會影響進程中其他線程的errno值。
    我們知道一個進程中的所有線程都可以訪問這個進程的整個地址空間。除了使用寄存器以外,一個線程沒有辦法阻止另一個線程訪問它的數據。線程特定數據也不例外。雖然底層的實現部分並不能阻止這種訪問能力,但管理線程特定數據的函數可以提高線程間的數據獨立性,使得線程不太容易訪問到其他線程的線程特定數據。
    在分配線程特定數據之前,需要創建該數據關聯的鍵(key)。這個鍵將用於獲取對線程特定數據的訪問。
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));

創建的鍵存儲在keyp指向的內存單元中,這個鍵可以被進程中的所有線程使用,每個線程被這個鍵與不同的線程特定數據地址進行關聯。創建新鍵時,每個線程的數據地址設爲空值。
除了創建鍵以外,還可以爲鍵關聯一個可選的析構函數。當這個線程退出時,如果數據地址已經被置爲非空值,那麼析構函數就會被調用,它唯一的參數就是該數據地址。線程正常退出時該析構函數被調用,若非正常退出則不被調用。
線程通常使用malloc爲線程特定數據分配內存。析構函數通常釋放已經分配的內存。如果線程沒有釋放內存之前就退出了,那麼這塊內存會丟失,即線程所屬的進程出現了內存泄漏。
對所有的線程,我們都可以通過調用pthread_key_delete來取消鍵與線程特定數據之間的關聯關係。

#include <pthread.h>
int pthread_key_delete(pthread_key_t *keyp);

注意調用pthread_key_delete並不會激活與鍵關聯的析構函數。要釋放任何與鍵關聯的線程特定數據值的內存,需要在應用程序中採取額外的步驟。

一旦創建鍵以後,就可以通過調用pthread_setspecific函數把鍵和線程特定數據關聯起來。可以通過pthread_getspecific函數獲得線程特定數據的地址。

#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);//若沒有線程特定數據值,則返回NULL;
int pthread_setspecific(pthread_key_t key,const void *value);

12.7 取消選項

有兩個線程屬性並沒有包含在pthread_attr_t結構中,它們是可取消狀態可取消類型。這兩個屬性影響着線程在響應pthread_cancel函數調用時所呈現的行爲。
線程可以通過調用pthread_setcancelstate修改它的可取消狀態。

#include <pthread.h>
int pthread_setcancelstate(int state,int *oldstate);

state可設置爲PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE.
pthread_cancel調用並不等待線程終止。在默認情況下,線程在取消請求發出以後還是繼續運行,直到線程到達某個取消點(就是一些函數)。
如果應用程序很長時間都會不會調用以上函數(如數學計算),那麼可以調用pthread_testcancel函數在程序中添加自己的取消點。

#include <pthread.h>
void pthread_testcancel(void);

調用pthread_testcancel時,如果某個取消請求正處於掛起狀態而且取消並沒有置爲無效那麼線程就被取消。但是如果取消被置爲無效,pthread_testcancel調用就沒有任何效果了。


我們所描述的默認的取消類型也稱爲推遲取消。調用pthread_cancel以後,在線程到達取消點之前,並不會出現真正的取消。可以通過調用pthread_setcanceltype 來修改取消類型。

#include <pthread.h>
int pthread_setcanceltype(int type,int *oldtype);

pthread_setcanceltype函數把取消類型設置爲type,其類型可以是PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS(異步取消)。
異步取消與推遲取消不同,因爲使用異步取消時,線程可以在任意時刻撤銷,不是非得遇到取消點才能被取消。

12.8 線程和信號

每個進程都有自己的信號屏蔽字,但是信號的處理是進程中所有線程共享的
進程中的信號是遞送到單個線程的。如果一個信號與硬件故障有關,那麼該信號一般會被髮送到引起該事件的線程中去,而其他信號則被發送到任意一個線程
線程中需要使用pthread_sigmask來阻止信號發送。

#include <signal.h>
int pthread_sigmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);

線程可以通過調用sigwait等待一個或多個信號的出現:

#include <signal.h>
int sigwait(const sigset_t *restrict set,int *restrict signop);

set參數指定了線程等待的信號集。返回時signop指向的整數包含發送的信號。
如果信號集中的某個信號在sigwait調用的時候處於掛起狀態,那麼sigwait將無阻塞地返回,在返回之前,sigwait將從進程中移除那些處於掛起等待狀態的信號。如果具體實現支持排隊信號,並且信號的多個實例被掛起,那麼sigwait將會移除該信號的一個實例,其他實例還要繼續排隊。
使用sigwait的好處在於他可以簡化信號處理,允許把異步產生的信號用同步的方式處理。爲了防止信號中斷線程,可以把信號加到每個線程的信號屏蔽字中。然後可以安排專用的線程處理信號。!!!

  • 如果多個線程在sigwait的調用中因等待同一個信號而阻塞,那麼在信號遞送的時候,就只有一個線程可以從sigwait中返回。
  • 如果一個信號被捕獲,而且一個線程正在sigwait調用中等待同一信號,那麼這時將由操作系統實現來決定以何種方式遞送信號。操作系統可以讓sigwait返回,也可以激活信號處理程序,但這兩者不會同時發生。

要想把信號發送給進程,可以調用kill。要把信號發送給線程,可以調用pthread_kill

#include <pthread.h>
int pthread_kill(pthread_t thread,int signo);

12.9 線程和fork

當線程調用fork時,就爲子進程創建了整個進程地址空間的副本。子進程通過繼承整個地址空間的副本,還從父進程那繼承了每個互斥量、讀寫鎖和條件變量的狀態。如果父進程包含一個以上的進程,子進程在fork返回後,如果緊接着不是馬上調用exec的話,就需要清理鎖狀態。
在子進程內部,只存在一個線程,它是由父進程中調用fork線程的副本構成的。如果父進程中的線程佔用鎖,子進程將同樣佔有這些鎖。問題是子進程並不包含佔有鎖線程的副本,所以子進程就沒辦法知道它佔有了哪些鎖,需要釋放哪些鎖。如果fork之後立即使用exec替換地址空間,就不會有這個問題。
要清除鎖狀態,可以通過調用pthread_atfork函數建立fork處理程序。

#include <pthread.h>
int pthread_atfork(void(*prepare)(void),void(*parent)(void),void(*child)(void));
  • prepare fork處理程序由父進程在fork創建子進程前調用,其任務是獲得父進程所有鎖。
  • parent 此fork處理程序是在fork創建子進程以後、返回之前在父進程上下文中調用的,其任務是對prepare處理程序中獲得的鎖進行解鎖。
  • child 這個處理程序是在fork返回之前在子進程上下文中調用的。其作用也是釋放prepare獲得的鎖。

12.10 線程和IO

3.11節介紹了pread和pwrite函數。這些函數在多線程環境下是非常有用的,因爲進程中的所以線程共享相同的文件描述符。
考慮以下情況:
線程A:

lseek(fd,300,SEEK_SET);
read(fd,buf1,100);

線程B:

lseek(fd,700,SEEK_SET);
read(fd,buf2,100);

如果線程A執行lsek之後線程B在線程A調用read之前調用lseek,那麼兩個線程最終會讀取同一條記錄。這顯然不是我們希望的。
位解決這個問題,可以使用pread,使偏移量的設定和讀取稱爲一個原子操作。

pread(fd,buf,100,300);

12.11 小結

在UNIX系統中,線程提供了分解併發任務的另一種模型。
線程促進了獨立控制線程之間的共享,但也出現了它特有的同步問題。
本章中:

  • 我們瞭解瞭如何調整線程和它們的同步原語;
  • 討論了線程的可衝入性;
  • 學習了線程如何與其他面向進程的系統調用進行交互。
發佈了55 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章