天天看點

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

可用sysconf函數查詢的線程限制:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

但有些系統沒提供通路這些限制的方法,但這些限制仍存在。

我們可以設定線程和線程屬性或互斥量和互斥量屬性來細調線程和同步對象的行為,管理這些屬性的函數都遵循相同的模式:

1.每個對象與它自己類型的屬性進行關聯(即線程與線程屬性關聯,互斥量與互斥量屬性關聯)。一個屬性對象可包含多個屬性。應用程式不需了解屬性對象内部結構的細節,這樣可以增強程式可移植性(需要函數管理這些屬性對象)。

2.有一個初始化函數将屬性設為預設值。

3.由銷毀屬性對象的函數,如果初始化函數配置設定了與屬性對象關聯的資源,銷毀函數負責釋放這些資源。

4.每個屬性都有一個從屬性對象中擷取屬性值的函數。

5.每個屬性都有一個設定屬性值的函數,屬性值按值傳遞。

pthread_create函數中,可傳入指向pthread_attr_t結構的指針,用來修改線程預設屬性,并把這些屬性與建立的線程關聯起來。使用以下函數初始化和反初始化pthread_attr_t結構:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

調用pthread_attr_init後,pthread_attr_t就變成作業系統實作支援的線程屬性的預設值。

pthread_attr_destroy函數反初始化pthread_attr_t,如果pthread_attr_init的實作對該屬性對象的記憶體空間是動态配置設定的,則反初始化時會釋放該空間,并将屬性對象值設為無效的值,此時,如果該屬性對象被誤用,會導緻pthread_create函數傳回錯誤碼。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

如果對某個線程的終止狀态不感興趣,可用pthread_detach函數讓作業系統線上程退出時收回它所占用的資源。

如果線上程建立時就知道不需要了解線程的終止狀态,可修改pthread_attr_t結構中的detachstate線程屬性,讓線程開始時就處于分離狀态。

可用pthread_attr_setdetachstate函數将detachstate設成以下值之一:PTHREAD_CREATE_DETACHED(以分離狀态啟動線程)、PTHREAD_CREATE_JOINABLE(正常啟動線程):

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

以分離狀态建立線程:

#include <pthread.h>

int makethread(void *(*fn)(void *), void *arg) {    // 第一個參數是線程入口函數指針,傳回類型為void *;第二個參數是要傳給線程入口函數的參數
    int err;
    pthread_t tid;
    pthread_attr_t attr;

    err = pthread_attr_init(&attr);
    if (err != 0) {
        return err;
    }
    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err == 0) {
        err = pthread_create(&tid, &attr, fn, arg); 
    }
    pthread_attr_destroy(&attr);
    return err;
}
           

上例忽略了pthread_attr_destroy函數的傳回值,我們對線程屬性進行了合理的初始化,應該不會失敗,但有可能失敗,最壞的情況就是初始化線程屬性時動态配置設定的空間會丢失,造成記憶體洩漏,此時已經沒有補救措施了,對線程屬性進行清理的唯一接口就是pthread_attr_destroy函數,但它失敗了。

POSIX的作業系統不一定支援線程棧屬性,但SUS的XSI的作業系統一定支援線程棧屬性。可在編譯階段使用_POSIX_THREAD_ATTR_STACKADDR或_POSIX_THREAD_ATTR_STACKSIZE符号檢查系統是否支援線程棧屬性,如定義了其中一個,就支援。也可在運作階段把_SC_THREAD_ATTR_STACKADDR或_SC_THREAD_ATTR_STACKSIZE傳給sysconf函數檢查支援情況。

使用以下函數對線程棧屬性進行管理:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

對程序來說,虛位址空間大小固定。但對線程來說,同樣大小的虛位址空間被所有線程棧共享,如果有很多線程,以緻這些線程棧的大小加起來超出了可用的虛位址空間,就需要減少預設的線程棧大小;還可能線程建立了大量的自動變量,或調用的函數涉及許多很深的棧幀,那麼幀大小可能比預設幀大小更大。

如果線程棧的虛位址空間都用完了,可用malloc或mmap函數配置設定空間替代原空間,并調用pthread_attr_setstack來改變建立的棧的位置。由stackaddr參數指定的位址用作線程棧的記憶體範圍中的最低可尋址位址,該位址與處理器結構相應的邊界應對齊。

pthread_attr_t結構中的線程屬性成員stackaddr被定義為棧的最低記憶體位址,這并不一定是棧的開始位置,如果棧是從高位址向低位址方向增長的(如x86模型),則stackaddr線程屬性是棧的結尾位置而非開始位置。

使用以下函數隻改變或擷取線程屬性stacksize:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

可用以上函數改變預設的棧大小,且不用自己處理線程棧的配置設定問題。設定stacksize屬性時,其值不能小于PTHREAD_STACK_MIN的值。

線程屬性guardsize控制線程棧末尾之後的用以避免棧溢出的擴充記憶體大小。此屬性預設值取決于具體實作,常用值是系統頁大小。可把guardsize設為0,此時不會提供警戒緩沖區。如果修改了線程屬性stackaddr,系統就認為我們将自己管理棧,這使得警戒緩沖區機制無效,等同于把guardsize設為0。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

如果guardsize線程屬性被修改,作業系統可能會将其取為頁大小的整數倍。當線程的棧指針溢出到警戒區域,應用可能通過信号接收到出錯資訊。

還有其他的一些線程屬性。

互斥量屬性用結構pthread_mutexattr_t結構表示。可用以下函數初始化和反初始化互斥量:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

pthread_mutexattr_init函數用預設的互斥量屬性初始化pthread_mutexattr_t結構。它有一種屬性是程序共享屬性,在POSIX.1中是可選的,可通過檢查是否定義了_POSIX_THREAD_PROCESS_SHARED判斷平台是否支援程序共享屬性,也可以運作時把_SC_THREAD_PROCESS_SHARED參數傳給sysconf進行檢查。遵循XSI的系統一定支援此選項。

預設,多個線程可以通路同一個程序中的同一個同步對象,此時,程序共享互斥量屬性設為PTHREAD_PROCESS_PRIVATE。還有一種機制是允許互相獨立的多個程序把同一個記憶體資料塊映射到它們各自獨立的位址空間中,就像多個線程通路共享資料一樣, 多個程序通路共享資料也需同步,此時,可将互斥量屬性程序共享設為PTHREAD_PROCESS_SHARED,于是從多個程序共享的記憶體資料塊中配置設定的互斥量就可用于這些程序的同步。

使用以下函數查詢和修改互斥量的程序共享屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

互斥量的程序共享屬性設為PTHREAD_PROCESS_PRIVATE時,pthread線程庫會提供更有效率的互斥量實作。

使用以下函數擷取互斥量的健壯屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

在多程序間共享互斥量時,當鎖定互斥量的程序終止時,其他阻塞在這個鎖的程序将會一直阻塞下去。

健壯屬性有兩種可能的取值,預設值是PTHREAD_MUTEX_STALLED,此時持有互斥量的程序終止時不采取特别的動作,等待該互斥量解鎖的程序會一直等待。另一個取值為PTHREAD_MUTEX_ROBUST,此時會導緻阻塞的線程從pthread_mutex_lock函數傳回并擷取鎖,但傳回值為EOWNERDEAD而非0,這個傳回值不代表錯誤,因為調用者已經擁有了鎖。這個傳回值訓示應用應恢複互斥量保護的狀态。

使用健壯的互斥量時,pthread_mutex_lock函數的傳回值為三種:

1.不需要恢複的成功。

2.需要恢複的成功。

3.失敗。

如果應用的互斥量保護的狀态無法恢複,線程對互斥量解鎖後,該互斥量将處于永久不可用狀态,為避免這種情況,必須在解鎖前調用以下函數指明該互斥量相關的狀态是一緻的:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

如果線程沒有先調用pthread_mutex_consistent就解鎖了互斥量,那麼其他擷取該互斥量時阻塞的線程會得到錯誤碼ENOTRECOVERALBLE,如果發生這種情況,互斥量就不再可用。

互斥量的類型屬性控制着互斥量的鎖定特點,POSIX.1定義了4種類型:

1.PTHREAD_MUTEX_NORMAL。标準互斥量類型,不做特殊的錯誤檢查和死鎖檢測。

2.PTHREAD_MUTEX_ERRORCHECK。此互斥量類型提供錯誤檢查,檢查的情況如下表。

3.PTHREAD_MUTEX_RECURSIVE。此互斥量類型允許同一線程在互斥量解鎖前對該互斥量多次加鎖。遞歸互斥量維護鎖的計數,隻有解鎖次數等于加鎖次數時,才解鎖成功。

4.PTHREAD_MUTEX_DEFAULT。提供預設特性和行為,作業系統實作它時,可将它映射為以上三種中的任一種類型。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

不占用時解鎖指解鎖另一個線程加鎖的互斥量。

已解鎖時解鎖指解鎖未鎖定的互斥量。

使用以下函數擷取和設定互斥量的類型屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制
UNIX環境進階程式設計 學習筆記 第十二章 線程控制
UNIX環境進階程式設計 學習筆記 第十二章 線程控制

假設func1和func2是函數庫中現有函數,其接口不能改變,于是我們将互斥量嵌入到了資料結構中,并把這個結構的位址作為參數傳入。如果func1和func2都要操作這個結構,可能會有一個以上的線程同時通路該資料結構,那麼func1和func2必須在操作資料前對互斥量加鎖。

如果func1必須調用func2(如上圖),這時如果互斥量不是遞歸類型的,就會出現死鎖。如果能在func1調用func2前釋放互斥量,在func2傳回後再重新擷取互斥量,就可以避免使用遞歸互斥量,但這給了其他線程競争互斥量的機會,使得其他線程也修改了這個結構,這可能會造成資料失效,因為在func1還沒修改完此結構時,它就被其他線程修改了。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

如上圖,通過提供func2函數私有的、不用加鎖的版本,可保持func1和func2函數接口不變,且避免了使用遞歸互斥量。

如果可以改變函數接口,可在func2函數的參數中增加一個參數以說明這個結構是否被調用者鎖定,如鎖定,則不用再加鎖。

使用遞歸互斥量的另一種情況,有一個逾時函數,它安排另一個函數在某一時間運作,可為每個挂起的逾時函數建立一個線程,在時間未到時一直等待:

#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
using namespace std;

int makethread(void *(*fn)(void *), void *arg) {    // 第一個參數是線程入口函數指針,傳回類型為void *;第二個參數是要傳給線程入口函數的參數
    int err;
    pthread_t tid;
    pthread_attr_t attr;

    err = pthread_attr_init(&attr);
    if (err != 0) {
        cout << "init pthread_attr_t error" << endl;
        return err;
    }
    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err == 0) {
        err = pthread_create(&tid, &attr, fn, arg); 
		cout << "create pthread success" << endl;
    }
    pthread_attr_destroy(&attr);
    return err;
}

struct to_info {
    void (*to_fn)(void *);    // function
    void *to_arg;    // argument
    struct timespec to_wait;    // time to wait
}; 

#define SECTONSEC 1000000000    // seconds to nanoseconds

#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM))
#endif

#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000    // microseconds to nanoseconds

void clock_gettime(int id, struct timespec *tsp) {
    struct timeval tv;

    gettimeofday(&tv, NULL);
    tsp->tv_sec = tv.tv_sec;
    tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif

void *timeout_helper(void *arg) {
    struct to_info *tip;
    printf("in timeout_helper");
    tip = (struct to_info *)arg;
    clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
    printf("after nanosleep");
    (*tip->to_fn)(tip->to_arg);
    free(arg);
    return 0;
}

void timeout(const struct timespec *when, void (*func)(void *), void *arg) {
    struct timespec now;
    struct to_info *tip;
    int err;

    clock_gettime(CLOCK_REALTIME, &now);
    cout << "when sec: " << when->tv_sec << " nsec: " << when->tv_nsec << endl;
    cout << "now  sec: " << now.tv_sec << " nsec: " << now.tv_nsec << endl;
    if ((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
        tip = (to_info *)malloc(sizeof(struct to_info));
		if (tip != NULL) {
		    printf("going to set to_info\n");
		    tip->to_fn = func;
		    tip->to_arg = arg;
		    tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
		    if (when->tv_nsec >= now.tv_nsec) {
		        tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
		    } else {
		        tip->to_wait.tv_sec--;
				tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
		    }
		    err = makethread(timeout_helper, (void *)tip);
		    if (err == 0) {
		        return;
				cout << "make thread success" << endl;
		    } else {
		        free(tip);
				cout << "make thread error, err = " << err << endl;
		    }
		}
    }
    printf("when <= now");

    // we get there if (a) when <= now, or (b) malloc fails, or (c) we can't make a thread
    // so we just call the function now
    (*func)(arg);
}

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

void retry(void *arg) {
    pthread_mutex_lock(&mutex);
    // perform retry steps
    printf("in retry\n");
    pthread_mutex_unlock(&mutex);
}

int main() {
    int err, condition = 1, arg;
    struct timespec when;

    if ((err = pthread_mutexattr_init(&attr)) != 0) {
        printf("%d, pthread_mutexattr_init failed!", err);
        exit(1);
    }
    if ((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0) {
        printf("%d, can't set recursive type", err);
		exit(1);
    }
    if ((err = pthread_mutex_init(&mutex, &attr)) != 0) {
        printf("%d, can't create recursive mutex", err);
        exit(1);
    }

    // continue processing
    
    pthread_mutex_lock(&mutex);    // 此處上鎖使檢查condition和執行timeout成為一個原子操作
    if (condition) {
        clock_gettime(CLOCK_REALTIME, &when);
		when.tv_sec += 10;    // 10 seconds from now
		timeout(&when, retry, (void *)((unsigned long)arg));
    }
    pthread_mutex_unlock(&mutex);
    sleep(15);    // 保證建立的線程能等到睡眠結束并執行完程式,或主線程還可進行其他操作
    exit(0);
}
           

檢查condition的鎖和要執行的函數中的鎖是一個,是以當timeout中出問題時,如不能建立線程、安排函數運作的時間已過、malloc調用失敗時,timeout可以直接在最後調用函數,此時由于使用了遞歸互斥量,避免死鎖或要傳回main函數解鎖後再調用該函數。正常情況應該是main函數調用timeout,timeout函數調用makethread建立線程,之後建立的線程等待時間到來,而函數makethread傳回函數timeout,然後timeout函數傳回main函數,之後再在main中解鎖互斥量,然後時間到達後,進入要執行的函數,在其中再加鎖運作。

我們建立的是分離的線程,這是由于retry函數将在未來運作,我們不希望調用pthread_join阻塞自己空等線程。

使用以下函數初始化和反初始化讀寫鎖屬性結構:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

讀寫鎖隻支援程序共享屬性,它與互斥量的程序共享屬性是相同的,使用以下函數用于讀取和設定讀寫鎖的程序共享屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

初始化和反初始化條件變量:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

SUS定義了條件變量的兩個屬性:程序共享屬性和時鐘屬性,擷取和改變它們:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制
UNIX環境進階程式設計 學習筆記 第十二章 線程控制

條件變量的時鐘屬性控制計算pthread_cond_timewait函數的逾時參數時采用的是哪個時鐘。

SUS并沒有為其他有逾時等待函數的同步對象的屬性對象定義時鐘屬性。

使用以下函數初始化和反初始化屏障屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

屏障屬性隻有程序共享屬性:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

程序共享屬性的值可以是PTHREAD_PROCESS_SHARED或PTHREAD_PROCESS_PRIVATE。

線程類似于信号處理程式,可能同時通路一個函數兩次。

如果一個函數在一個時間點可以被多個線程使用,就稱該函數是線程安全的。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

除了以上函數不是線程安全的之外,傳入空指針的ctermid和tmpnam函數也不是線程安全的。

如果wcrtomb和wcsrtombs的參數mbstate_t傳入的是空指針,也不能保證是線程安全的。

支援線程安全函數的系統會在unistd.h中定義符号_POSIX_THREAD_SAFE_FUNCTIONS。也可以給sysconf函數傳入_SC_THREAD_SAFE_FUNCTIONS參數在運作時檢查是否支援線程安全的函數。

SUSv4之前,所有遵循XSI的實作都必須支援線程安全函數,但在SUSv4中,線程安全函數的支援必須考慮遵循POSIX。

對于POSIX.1中的一些非線程安全函數,會提供可替代的線程安全版本:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

這些函數比它們的非線程安全版本的名字多了_r結尾,表明這些版本是可重入的。很多函數不是線程安全的原因在于它們傳回的資料存放在靜态的記憶體緩沖區。通過修改接口,要求調用者自己提供緩沖區可以使函數變為線程安全的。

如果一個函數對多個線程來說是可重入的,那麼這個函數就是線程安全的。但這并不能說明對信号處理程式來說,該函數也是可重入的。如果函數對異步信号程式的重入是安全的,那麼可以說函數是異步信号安全的。

除了上圖可替代的線程安全函數外,POSIX.1還提供了以線程安全方式管理FILE對象的方法,可用flockfile和ftrylockfile函數擷取給定FILE對象關聯的鎖,這個鎖是遞歸的。要求所有操作FILE對象的标準IO例程的動作看起來就像在它們内部調用了flockfile和funlockfile。

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

以上函數允許應用把多個對标準IO函數的調用組合成原子序列。

遞歸鎖隻是首次加鎖的線程可以多次加鎖,加鎖線程未解鎖時其他線程不能加鎖。

如果标準IO例程都擷取它們各自的鎖,那麼在做一次一個字元的IO時,性能會下降很多,此時,需要對每個字元的讀寫操作進行擷取鎖和釋放鎖的動作,以下是不加鎖版本的基于字元的标準IO例程:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

除非被flockfile或ftrylockfile的調用包圍,否則盡量不要用這4個函數,這可能會引起其他控制線程非同步地通路資料導緻的問題。

一旦對FILE對象加鎖,就能在釋放鎖之前對這些函數進行多次調用,這樣可以在多次的資料讀寫上分攤總的加解鎖開銷。

getenv函數的非可重入版本,當兩個線程同時調用它時,會看到不一緻的結果,因為所有調用getenv的線程傳回的字元串都存在同一個靜态緩沖區中:

#include <limits.h>
#include <string.h>

#define MAXSTRINGSZ 4096

static char envbuf[MAXSTRINGSZ];

extern char **environ;

char *getenv(const char *name) {
    int i, len;

    len = strlen(name);
    for (i = 0; environ[i] != NULL; ++i) {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
		    strncpy(envbuf, &environ[i][len + 1], MAXSTRINGSZ - 1);
		    return envbuf;
		}
    }

    return NULL;
}
           

以下是getenv函數的可重入版本,它使用pthread_once函數來確定不管多少線程同時競争getenv_r函數,每個程序隻調用thread_init一次:

#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>

extern char **environ;

pthread_mutex_t env_mutex;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;

static void thread_init() {
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&env_mutex, &attr);
    pthread_mutexattr_destroy(&attr);
}

int getenv_r(const char *name, char *buf, int buflen) {
    int i, len, olen;

    pthread_once(&init_done, thread_init);
    len = strlen(name);
    pthread_mutex_lock(&env_mutex);
    for (i = 0; environ[i] != NULL; ++i) {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
		    olen = strlen(&environ[i][len + 1]);
		    if (olen >= buflen) {
		        pthread_mutex_unlock(&env_mutex);
				return ENOSPC;
		    }
		    strcpy(buf, &environ[i][len + 1]);
		    pthread_mutex_unlock(&env_mutex);
		    return 0;
		}
    }
    pthread_mutex_unlock(&env_mutex);
    return ENOENT;    // 沒有這樣的檔案或目錄錯誤
}
           

調用者提供自己的緩沖區,這樣每個線程可以使用各自不同的緩沖區以避免其他線程的幹擾。要想使getenv_r函數成為線程安全的,還需在搜尋請求的字元時避免其他線程的幹擾,以上我們可以使用一個互斥量,通過getenv_r函數來通路環境清單(putenv函數使用前後也會操作此互斥量),來使得通路是線程安全的。

可以使用讀寫鎖,進而允許對getenv_r函數進行并發通路,但可能并不會很大程度上改善性能,這是由于:第一,環境清單通常不會很長,掃描清單時不需要長時間占有互斥量;第二,對getenv和putenv的調用也不是頻繁發生的,是以改善對環境清單的通路不會對程式整體性能産生很大影響。

即使getenv_r函數是線程安全的,這也不意味着它對信号處理程式是可重入的,如果使用非遞歸互斥量,線程從信号處理程式中調用getenv_r就可能出現死鎖。如果信号處理程式線上程調用getenv_r時中斷了該線程,此時我們已占有加鎖的env_mutex,這樣信号處理程式試圖對這個互斥量加鎖時會被阻塞,最終導緻線程進入死鎖狀态。是以,我們必須使用遞歸互斥量。

pthread函數不能保證是異步信号安全的,是以不能把pthread函數用于其他想設計成異步信号安全的函數中。

線程特定資料也被稱為線程私有資料,是存儲和查詢某個特定線程相關資料的一種機制。我們使用它可以使每個線程通路它自己單獨的資料副本,而不需擔心與其他線程的同步通路問題。

使用線程特定資料的原因有兩個:

1.有時候我們需要維護基于每個線程的資料,而線程ID不能保證是小而連續的整數,是以就不能簡單地使用線程ID作為每個線程資料組成的數組的下标。即使線程ID是小而連續的整數,我們還需額外的保護,使得某個線程的資料不會與其他線程的資料相混淆。

2.它讓基于程序的接口适應多線程的環境機制。比如errno,線程出現之前的接口把errno定義為程序上下文中全局可通路的整數,在系統調用或庫例程調用或執行失敗時設定errno,把它作為操作失敗的附屬結果。而現在errno被定義為線程私有資料,這樣一個線程設定了errno也不會影響到其他線程中的errno值。

一個程序中的所有線程都能通路這個程序整個位址空間,除了使用寄存器,一個線程無法阻止另一個線程通路它的資料,線程特定資料也不例外,雖然底層實作不能阻止這種通路能力,但管理線程特定資料的函數也能提高線程間的獨立性,使線程不太容易通路到其他線程的線程特定資料。

配置設定線程特定資料前,要先建立與該資料關聯的鍵,鍵将用于擷取對線程特定資料的通路,建立一個鍵:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

建立的鍵存儲在參數keyp指向的記憶體單元,這個鍵可被程序中所有線程使用,但每個線程把這個鍵與不同的特定資料位址進行關聯。建立新鍵時,每個線程的資料位址都被設為空值。

參數destructor是一個可選的、與鍵關聯的析構函數,當一個線程退出時,如果資料位址是非空值,那麼析構函數就會調用,該析構函數的參數就是資料位址,如果析構函數為空,則沒有析構函數與該鍵關聯。當線程調用pthread_exit或線程執行傳回、正常退出時,析構函數就會被調用;線程取消時,隻有在最後的清理處理程式傳回後,析構函數才被調用;線程調用exit、_exit、_Exit或abort,或出現其他非正常退出情況時,不會調用析構函數。

線程通常用malloc函數為線程特定資料配置設定記憶體,析構函數通常釋放已配置設定的記憶體。如果線程沒有釋放記憶體就退出了,那麼這塊記憶體就會丢失,線程所屬程序就出現了記憶體洩漏。

每個作業系統的鍵數量有限制(PTHREAD_KEYS_MAX)。

線程退出時,線程特定資料的析構函數按OS的實作定義的順序被調用,析構函數可能會調用另一個函數,該函數可能會建立新的線程特定資料,并且把這個資料與目前鍵關聯起來,當所有析構函數調用完成後,系統會檢查是否還有非空的線程特定資料與鍵關聯,如果有,再次調用析構函數,這個過程會一直重複直到線程所有鍵都是空的線程特定值或已經做了PTHREAD_DESTRUCTOR_ITERATIONS定義的最大次數的嘗試。

取消鍵和線程特定資料之間的關聯:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

但該函數不會調用與鍵關聯的析構函數。

配置設定的鍵可能由于初始化階段的競争而發生變動:

void destructor(void *);

pthread_key_t key;
int init_done = 0;

int threadfunc(void *arg) {
    if (!init_done) {
        init_done = 1;
        err = pthread_key_create(&key, destructor);
    }
}
           

有些線程可能看到一個鍵值,而其他線程可能看到其他鍵值,這取決于系統是如何排程線程的。解決這種競争的方法是調用pthread_once:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制
UNIX環境進階程式設計 學習筆記 第十二章 線程控制

參數intflag必須是一個非本地變量(如全局變量或靜态變量),且必須初始化為PTHREAD_ONCE_INIT。

如果每個線程都調用pthread_once,那麼隻有首次調用它的線程能成功。

建立鍵時避免出現沖突的方法:

void destructor(void *);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;

void thread_init() {
    err = pthread_key_create(&key, destructor);
}

int threadfunc(void *arg) {
    pthread_once(&init_done, thread_init);
}
           

建立好鍵後,就可以把鍵和線程特定資料關聯起來:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

如果沒有線程特定資料與鍵關聯,pthread_getspecific函數傳回空指針,可通過此值确定是否需要調用pthread_setspecific。

用線程特定資料維護每個線程的資料緩沖區副本,存放各自傳回字元串的geten函數:

#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>

#define MAXSTRINGSZ 4096

static pthread_key_t key;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;

extern char **environ;

static void thread_init() {
    pthread_key_create(&key, free);
}

char *getenv(const char *name) {
    int i, len;
    char *envbuf;

    pthread_once(&init_done, thread_init);    // 確定第一個調用的線程建立鍵
    pthread_mutex_lock(&env_mutex);
    envbuf = (char *)pthread_getspecific(key);
    if (envbuf == NULL) {    // 如果是空指針,需要先關聯鍵和線程特定資料
        envbuf = malloc(MAXSTRINGSZ);
		if (envbuf == NULL) {
		    pthread_mutex_unlock(&env_mutex);
		    return NULL;
		}
		pthread_setspecific(key, envbuf);    // 關聯鍵和線程特定資料
    }
    len = strlen(name);
    for (i = 0; environ[i] != NULL; ++i) {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
		    strncpy(envbuf, &environ[i][len + 1], MAXSTRINGSZ - 1);
		    pthread_mutex_unlock(&env_mutex);
		    return envbuf;
		}
    }
    pthread_mutex_unlock(&env_mutex);
    return NULL;
}
           

這個版本的getenv函數是線程安全的,但不是異步信号安全的,對信号處理程式而言,即使使用了遞歸互斥量,但其中調用了malloc,malloc函數本身不是異步信号安全的。

線程屬性可取消狀态和可取消類型沒有包含在pthread_attr_t結構中,這兩個屬性影響着線程在響應pthread_cancel函數時呈現的行為。

可取消狀态屬性可取兩個值:PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。可通過以下函數修改:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

pthread_setcancelstate函數可把目前可取消狀态設為state,同時也可把原來的可取消狀态存放在參數oldstate指向的記憶體單元中,這兩步是一個原子操作。

pthread_canel調用并不等待要取消的線程終止,預設,要被取消的線程在取消請求發出後會繼續運作,直到要被取消的線程到達某個取消點,取消點是線程檢查它是否被取消的位置,如果取消了,則按請求行事。POSIX.1保證線程在調用以下函數時取消點會出現:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

線程啟動時預設的可取消狀态是PTHREAD_CANCEL_ENABLE,當狀态設為PTHREAD_CANCEL_DISABLE時,調用pthread_cancel不會殺死線程,而是挂起取消請求,當取消狀态再次變為PTHREAD_CANCEL_ENABLE時,線程将在下一個取消點上處理挂起的取消請求。

如果線程長時間不會調用一些有取消點的函數,可用以下函數添加取消點:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

調用以上函數時,如果有某個取消請求正處于挂起狀态,且線程狀态屬性為可被取消(PTHREAD_CANCEL_ENABLE),則線程就會被取消,如果線程取消屬性為不可被取消(PTHREAD_CANCEL_DISABLE),則該函數調用沒有任何效果。

可通過以下函數設定取消類型:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

取消類型可以是PTHREADCANCEL_DEFERRED(預設,也稱推遲取消,線上程到達取消點前,不會真正取消)和PTHREAD_CANCEL_ASYNCHRONOUS(不用遇到取消點就能被取消)。

pthread_setcanceltype函數會把原來的取消類型存放到oldtype參數指針指向的整型單元中。

每個線程都有自己的信号屏蔽字,但信号的處理是程序中所有線程共享的,這意味着當某個線程修改了與某個給定信号相關的處理行為後,所有線程都共享這個處理行為的改變。

程序中的信号是遞送給單個線程的,如果一個信号與硬體故障相關,那麼該信号一般會發送到引起該事件的線程中,其他信号會被發送到任意一個線程。

程序使用sigprocmask函數阻止信号發送,但該函數行為在多線程的程序中沒有定義,線程必須使用pthread_sigmask函數:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

pthread_sigmask與sigprocmask函數基本相同,但它工作線上程中,且失敗時傳回錯誤碼(不像sigprocmask傳回-1并設定errno)。set參數包含線程要操作的信号集;how參數是以下3個值之一:

1.SIG_BLOCK:把參數set表示的信号集中所有信号添加到線程信号屏蔽字中。

2.SIG_SETMASK:把線程屏蔽字設為參數set表示的信号集中的所有信号。

3.SIG_UNBLOCK:從線程信号屏蔽字中删除set表示的信号集中的信号。

如果oset參數不為空,把之前的信号屏蔽字存儲在該參數指向的sigset_t結構中。線程可将set參數設為NULL(此時how參數會被忽略),并傳入oset參數來擷取線程目前的屏蔽字。

線程調用sigwait可等待一個或多個信号的出現:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

set參數指定了線程要等待的信号集,傳回時将接收到的信号編号存放到signop參數指向的整型單元。

如果信号集中的某信号在調用sigwait時處于挂起狀态,則sigwait函數将無阻塞地傳回,傳回前,sigwait函數從程序挂起的信号集中移除此信号;如果實作支援排隊信号,且該信号的多個執行個體被挂起,那麼sigwait函數隻會移除該信号的一個執行個體,其他執行個體還要繼續排隊。

為了避免錯誤,線程在調用sigwait前,必須先阻塞它要等待的信号。sigwait函數會原子地取消在等待信号的阻塞和一個信号被遞送。在傳回前,sigwait函數會恢複線程的信号屏蔽字。如果信号在sigwait調用時沒有被阻塞,那麼線上程完成對sigwait的調用前會出現一個時間窗,此時信号就可以被發送給線程,進而進入該信号的信号處理程式或按系統預設方式處理該信号。

sigwait函數可簡化信号的處理,允許把異步産生的信号用同步的方式處理,為防止信号中斷線程,可将信号加到每個線程的信号屏蔽字中,然後安排專用的線程處理信号,處理信号的方式是進行正常的函數調用,而非傳統方式那樣可能會中斷某些函數的調用。

如果多個線程在sigwait調用中因等待同一個信号而阻塞,那麼在信号遞送時,隻有一個線程可以從sigwait函數傳回。如果一個信号有它自己的信号處理程式,同時一個線程在sigwait此信号,那麼信号如何遞送由作業系統實作決定,作業系統實作可以讓sigwit函數傳回,也可以激活信号處理程式,但這兩種情況不會同時發生。

kill函數把信号發給程序,pthread_kill把信号發給線程:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

當signo參數為0時,相當于檢查線程是否存在。如果信号的預設處理動作是終止程序,那麼該線程所在的整個程序會被殺死。

鬧鐘定時器是所有線程共享的。

當一個程序中的部分線程阻塞了某信号,此時信号發生,會遞送到未阻塞此信号的線程中:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

void *threadFunc(void *param) {
    sigset_t maskSigSet;
    if (sigemptyset(&maskSigSet) == -1) {
        printf("sigemptyset error\n");
        exit(1);
    }
    if (sigaddset(&maskSigSet, SIGALRM) == -1) {
        printf("sigaddset error\n");
		exit(1);
    }
    int err;
    if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
        printf("pthread_sigmask error\n");
		exit(1);
    }

    int sigRec = 0;
    if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
        printf("sigwait error\n");
		exit(1);
    }

    switch (sigRec) {
    case SIGALRM:
        printf("in threadFunc: SIGALRM received\n");
		break;
    }

    while (1) ;
}

int main() {
    int err = 0;
    pthread_t pid;
    if ((err = pthread_create(&pid, NULL, &threadFunc, NULL)) != 0) {
        printf("pthread_create error\n");
        exit(1);
    }

    alarm(2);

    while(1) ;

    exit(0);
}
           

由于SIGALRM預設行為是終止程序,是以當SIGALRM被遞送到阻塞并等待此信号的線程時,程序不會結束,而當此信号被遞送到未阻塞此信号的線程時,程序會結束,多次運作以上程式:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

可見每次信号都被遞送到未阻塞此信号的線程。如果所有線程都阻塞了此信号:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

void *threadFunc(void *param) {
    sigset_t maskSigSet;
    if (sigemptyset(&maskSigSet) == -1) {
        printf("sigemptyset error\n");
        exit(1);
    }
    if (sigaddset(&maskSigSet, SIGALRM) == -1) {
        printf("sigaddset error\n");
		exit(1);
    }
    int err;
    if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
        printf("pthread_sigmask error\n");
		exit(1);
    }

    int sigRec = 0;
    if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
        printf("sigwait error\n");
		exit(1);
    }

    switch (sigRec) {
    case SIGALRM:
        printf("in threadFunc: SIGALRM received\n");
		break;
    }

    while (1) ;
}

int main() {
    int err = 0;
    pthread_t pid;
    if ((err = pthread_create(&pid, NULL, &threadFunc, NULL)) != 0) {
        printf("pthread_create error\n");
        exit(1);
    }

    alarm(2);

    sigset_t maskSigSet;
    if (sigemptyset(&maskSigSet) == -1) {
        printf("sigemptyset error\n");
        exit(1);
    }   
    if (sigaddset(&maskSigSet, SIGALRM) == -1) {
        printf("sigaddset error\n");
        exit(1);
    }   
    if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
        printf("pthread_sigmask error\n");
        exit(1);
    }   

    int sigRec = 0;
    if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
        printf("sigwait error\n");
        exit(1);
    }   

    switch (sigRec) {
    case SIGALRM:
        printf("in main: SIGALRM received\n");
        break;
    }  

    while(1) ;

    exit(0);
}
           

多次運作以上程式:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

當所有線程都阻塞此信号并等待此信号發生時,信号隻會被遞送到一個線程。

多線程下忽視SIGINT,同時正常處理SIGQUIT:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>

int quitflag;    // set nonzero by thread
sigset_t mask;

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER;

void *thr_fn(void *arg) {
    int err, signo;

    for (; ; ) {
        err = sigwait(&mask, &signo);
		if (err != 0) {
		    printf("sigwait error\n");
		    exit(1);
		}
		
		switch (signo) {
		case SIGINT:
		    printf("interrupt\n");
		    break;
	
		case SIGQUIT:
		    pthread_mutex_lock(&lock);
		    quitflag = 1;
		    pthread_mutex_unlock(&lock);
		    pthread_cond_signal(&waitloc);
		    return 0;
	
		default:
		    printf("unexpected signal %d\n", signo);
		    exit(1);
		}
    }
}

int main () {
    int err;
    sigset_t oldmask;
    pthread_t tid;

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0) {
        printf("pthread_sigmask error\n");
		exit(1);
    }

    err = pthread_create(&tid, NULL, thr_fn, 0);
    if (err != 0) {
        printf("can't create thread\n");
		exit(1);
    }

    pthread_mutex_lock(&lock);
    while (quitflag == 0) {    // 防止接收信号的線程已經接收到信号且調用了pthread_cond_signal,main中還未首次到達此處
        pthread_cond_wait(&waitloc, &lock);
    }
    pthread_mutex_unlock(&lock);

    // SIGQUIT has been caught and is now blocked; do whatever
    quitflag = 0;

    // reset signal mask which unblocks SIGQUIT
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        printf("SIG_SETMASK error\n");
		exit(1);
    }

    exit(0);
}
           

運作它:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

新線程會繼承現有信号屏蔽字。所有線程中隻有一個線程用于信号接收,在對主線程編碼時不必擔心來自這些信号的中斷。

線程調用fork時,會為子程序建立整個程序位址空間的副本,即會從父程序處繼承每個互斥量、讀寫鎖、條件變量的狀态,如果父程序包含一個以上的線程,子程序在fork調用傳回後,如果緊接着不是馬上調用exec,就需要清理鎖狀态。在子程序内部,隻有一個線程,它是由父程序中調用fork的線程的副本構成的,如果父程序中其他線程占有鎖,子程序将同樣占有這些鎖,但子程序中不包含占有鎖的線程的副本,是以子程序不知道它占有了哪些鎖、需要釋放哪些鎖。

POSIX.1聲明,在fork調用傳回和子程序調用其中一個exec函數之間,子程序隻能調用異步信号安全的函數,這限制了在exec前子程序能做的事,但還是沒有處理子程序中鎖狀态的問題。

可通過調用pthread_atfork建立fork處理程式,來清除鎖狀态:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

prepare參數指向的處理程式由父程序調用的fork建立子程序前調用,作用是擷取父程序定義的所有鎖;parent參數指向的處理程式在調用fork建立了子程序後,傳回前在父程序的上下文中調用,作用是解鎖prepare參數指向的處理程式擷取的所有鎖;child參數指向的處理程式在fork調用傳回前的子程序上下文中調用,作用是釋放prepare參數指向的處理程式擷取的所有鎖。

可多次調用pthread_atfork進而設定多套fork處理程式,如果不需要使用其中某個處理程式,可傳入空指針。使用多套fork處理程式時,parent和child處理程式是按它們注冊時的順序調用的,而prepare處理程式的調用順序與它們注冊時的順序相反,這樣可以允許多個子產品注冊它們自己的fork處理程式,且保持鎖的層次。

假如子產品A調用子產品B中的函數,且每個子產品都有自己的一套鎖,如果鎖的層次是A在B前,子產品B必須在子產品A前設定它的fork處理程式,父程序調用fork時,假設子程序先運作,會執行以下步驟:

UNIX環境進階程式設計 學習筆記 第十二章 線程控制

fork處理程式用來清除鎖狀态,對于條件變量的狀态,在有些作業系統中,條件變量可能不需做任何清理;但有些作業系統将鎖作為條件變量實作的一部分,此時條件變量就需要清理,此時調用fork後就不能使用條件變量。如果作業系統的實作是使用全局鎖保護程序中所有的條件變量資料結構,那麼作業系統實作本身可以在fork庫例程中做鎖的清理工作,但程式不應依賴作業系統實作中類似這樣的細節。

使用pthread_atfork函數: