APUE學習筆記——第十二章 線程控制

本章學習控制線程行爲方面的詳細內容,在上章中對線程屬性和同步原語屬性都取其默認行爲,接下來將介紹同一進程中的多個線程之間如何保持數據的私有性,最後討論基於進程的系統調用如何與線程進行交互。
1、線程限制: 
  Single Unix Specification定義了與線程操作有關的一些限制,和其他的限制一樣,可以通過sysconf來查詢。和其它的限制使用目的一樣,爲了應用程序的在不同操作 系統的可移植性。 一些限制: 
PTHREAD_DESTRUCTOR_ITERATIONS: 銷燬一個線程數據最大的嘗試次數,可以通過_SC_THREAD_DESTRUCTOR_ITERATIONS作爲sysconf的參數查詢。 
PTHREAD_KEYS_MAX: 一個進程可以創建的最大key的數量。可以通過_SC_THREAD_KEYS_MAX參數查詢。 
PTHREAD_STACK_MIN: 線程可以使用的最小的棧空間大小。可以通過_SC_THREAD_STACK_MIN參數查詢。 

PTHREAD_THREADS_MAX:一個進程可以創建的最大的線程數。可以通過_SC_THREAD_THREADS_MAX參數查詢


2、線程屬性
在上章中所有調用pthread_create函數的例子中,傳入的參數都是空指針,而不是指向pthread_attr_t結構的指針。可以使用pthread_attr_t結果修改線程默認屬性,並把這些屬性與創建的線程聯繫起來。可以使用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)
返回值:成功返回0,否則返回錯誤編號
POSIX.1線程屬性:1)detachstate:線程的分離狀態屬性;2)guardsize:線程棧末尾的警戒緩衝區大小(字節數);3)stackaddr:線程棧的最低地址;4)stacksize:線程棧的大小(字節數)
上章介紹了分離線程的概率,如果對現有的某個線程的終止狀態不感興趣的話,可以使用pthread_detach函數讓操作系統在線程退出時就收回它佔用的資源。
如果在創建線程時就知道不需要了解線程的終止狀態,則可以修改pthread_attr_t結構中的detachstate線程屬性,讓線程以分離狀態啓動。可以使用phtread_attr_setdetachstate函數把線程屬性detachstate設置爲以下兩個合法值之一:設置爲PTHREAD_CREATE_DETACHED,以分離狀態啓動線程;設置爲PTHREAD_CREATE_JOINABLE,正常啓動線程,應用程序可以獲取線程的終止狀態。
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate)
int phtread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

返回值:成功返回0,否則返回錯誤編號。

POSIX.1線程棧屬性的查詢和修改:
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *attr,void* *stackaddr,size_t *stacksize)
int pthread_attr_setstack(const pthread_attr_t *attr,void* stackaddr,size_t stacksize)//中文版的課本上stacksize前多個*,應該是翻譯錯了
這兩個函數既可以管理stackaddr線程屬性,也可以管理stacksize線程屬性。
int pthread_attr_getstacksize(const pthread_attr_t *attr,size_t *stacksize)
int pthread_attr_setstacksize(const pthread_attr_t *attr,size_t stacksize)
int pthread_attr_getguardsize(const pthread_attr_t *attr,size_t *guardsize)
int pthread_attr_setguardsize(const pthread_attr_t *attr,size_t guardsize)

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

void* thread_func(void* arg){
    pthread_attr_t attr;
    int detachstate;
    size_t guardsize;
    void* stackaddr;
    size_t stacksize;
    pthread_getattr_np(pthread_self(),&attr);
    pthread_attr_getdetachstate(&attr,&detachstate);
    pthread_attr_getstack(&attr,&stackaddr,&stacksize);
    pthread_attr_getguardsize(&attr,&guardsize);
    printf("Detachstate: %d\n",detachstate);
    printf("Stackaddr: %p\n",stackaddr);
    printf("Stacksize: %d bytes\n",stacksize);
    printf("Guardsize: %d bytes\n",guardsize);
    return (void*)0;
}

int main(){
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    //void* base = (void*)malloc(PTHREAD_STACK_MIN + 0x4000);
    //size_t size = PTHREAD_STACK_MIN + 0x4000;
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    //pthread_attr_setstack(&attr,base,size);
    pthread_create(&tid,&attr,thread_func,NULL);
    pthread_join(tid,NULL);
    sleep(2);
    exit(0);
}
Detachstate: 1
Stackaddr: 0xb6dd8000
Stacksize: 10489856 bytes
Guardsize: 4096 bytes

更多線程屬性
線程還有其他的一些屬性,這些屬性並不在pthread_attr_t結構中表達:可取消狀態、可取消類型、併發度
併發度控制着用戶級線程可以映射的內核線程或進程數目。
#include <pthread.h>
int pthread_getconcurrency();//返回當前的併發度
int pthread_setconcurrency(int level)

3、同步屬性
3.1、互斥量屬性
用pthread_mutexattr_init初始化pthread_mutexattr_t結構,用pthread_mutexattr_destroy來對該結構進行回收
int pthread_mutexattr_init(pthread_mutexattr_t *attr)
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr)
進程共享互斥量
在進程中,多個線程可以訪問同一個同步對象,這是默認行爲,在這種情況下,進程共享互斥量屬性需設置爲PTHREAD_PROCESS_PRIVATE
後面的章節將說明,存在這樣的機制,允許相互獨立的多個進程把同一個內存區域映射到它們各自獨立的地址空間中,就像多個線程訪問
共享數據一樣,多個進程訪問共享數據也需要同步。如果進程共享互斥量屬性設置爲PTHREAD_PROCESS_SHARED,從多個進程共享的
內存區域中分配的互斥量就可以用於這些進程的同步。
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);  //查詢進程共享屬性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared); //設置進程共享屬性
類型互斥量
類型互斥量屬性控制着互斥量的特性,POSIX.1定義了四種類型:
PTHREAD_MUTEX_NORMAL:標準互斥量類型,並不做任何特殊的錯誤檢查或死鎖檢測
PTHREAD_MUTEX_ERRORCHECK:提供錯誤檢查
PTHREAD_MUTEX_RECURSIVE:允許同一個線程在互斥量解鎖之前對該互斥量進行多次加鎖
PTHREAD_MUTEX_DEFFAULT:請求默認語義
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr,int *type); //查詢類型屬性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); //設置類型屬性
3.2讀寫鎖屬性
與互斥量類似,但是隻支持進程共享唯一屬性
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); 
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,int pshared);
3.3、條件變量屬性
只支持進程共享屬性
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr); 
int pthread_condattr_getpshared(const pthread_condattr_t *attr,int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);


4、重入

在遇到重入問題時線程與信號處理程序類似。有了信號處理程序和線程,多個控制線程在同一時間可能潛在的調用同一個函數。如果一個函數在同一時刻可以被多個線程安全地調用,就稱該函數是線程安全的。很多函數並不是線程安全的,因爲它們返回的數據是存放在靜態的內存緩衝區,可以通過修改接口,要求調用者自己提供緩衝區使函數變爲線程安全的。POSIX.1提供了以安全的方式管理FILE對象的方法,使用flockfile和ftrylockfile獲取與給定FILE對象關聯的鎖。這個鎖是遞歸鎖。
void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);
爲了避免標準I/O在一次一個字符操作時候頻繁的獲取鎖開銷,出現了不加鎖版本的基於字符的標準I/O例程
int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);

下面給出getenv函數的可重入版本(不能保證異步信號安全,但是線程安全)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.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);
}

void 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;
	    }
            strcpy(buf,&environ[i][len+1]);//取地址複製
	    pthread_mutex_unlock(&env_mutex);
	    return;
	}
    }
    pthread_mutex_unlock(&env_mutex);
    return;
}
void* thread_func1(void* arg){
    char buf[100];
    getenv_r("HOME",buf,100);
    printf("HOME=%s\n",buf);
    pthread_exit((void*)1);
}
void* thread_func2(void* arg){
    char buf[100];
    getenv_r("SHELL",buf,100);
    printf("SHELL=%s\n",buf);
    pthread_exit((void*)2);
}

int main(){
    pthread_t tid1,tid2;
    void* ret;
    pthread_create(&tid1,NULL,thread_func1,NULL);
    pthread_create(&tid2,NULL,thread_func2,NULL);
    pthread_join(tid1,&ret);
    pthread_join(tid2,&ret);
    exit(0);
}

5、線程私有數據

線程私有數據是存儲和查詢與某個線程相關的數據的一種機制。設置私有數據使每個線程可以單獨地訪問數據副本,而不需要擔心與其他線程的同步訪問問題。在分配線程私有數據之前,需要創建與該數據關聯的鍵。這個鍵將用於獲取對線程私有數據的訪問去。使用pthread_key_create創建一個鍵。
int pthread_key_create(pthread_key_t *keyp,void(*destructor)(void *))
除了創建鍵以外,pthread_key_create可以選擇爲該鍵關聯析構函數,當線程退出時,如果數據地址已經被置爲非NULL數值,那麼析構函數就會被調用,它唯一的參數就是該數據地址。如果傳入的destructor參數爲NULL,就表明沒有析構函數與鍵關聯。當線程調用pthread_exit或者線程執行返回,正常退出時,析構函數就會被調用。
線程通常使用malloc爲線程私有數據分配內存空間,析構函數通常釋放已分配的內存(free),如果線程沒有釋放內存就退出,那麼就會出現線程所屬進程出現內存泄漏。
線程可以爲線程私有數據分配多個鍵,鍵的數目有限制(PTHREAD_KEYS_MAX),每個鍵都可以有一個析構函數與它關聯。
對所有的線程都可以通過調用pthread_key_delete來取消鍵與線程私有數據之間的關聯關係。
int pthread_key_delete(pthread_key_y *key)
需要確保分配的鍵並不會由於在初始化階段的競爭而發生變動。下面代碼可以導致兩個線程都調用pthread_key_create

void destructor(void*)
pthread_key_t key;
int init_done = 0;
int thread_func(void* arg){
    if(!init_done){
	init_done = 1;
	err = pthread_key_create(&key,destructor);
    }
    ......
}
有些線程可能看到某個鍵值,而其他的線程看到的可能是另一個不同的鍵值,這個取決於系統是如何調度線程的,解決這種競爭的辦法是使用pthread_once。
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void(*initfn)(void))
initflag 必須是全局變量或靜態變量,而且必須初始化爲PTHREAD_ONCE_INIT。
如果每個線程都調用pthread_once,系統就能保證初始化例程initfn只被調用一次,即在系統首次調用pthread_once時。
創建鍵時避免出現競爭的一個恰當的方法可以描述如下:
void destructor(void*);
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(void){
    pthread_key_create(&key,destructor);
}
int thread_func(void* arg){
    pthread_once(&init_done,thread_init);
}
鍵一旦創建,就可以通過調用pthread_setspecific函數把鍵和線程私有數據關聯一起。通過pthread_getspecific函數獲得線程私有數據的地址。
void* pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key,const void* value);

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

static pthread_key_t key;
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);
    pthread_key_create(&key,free);
}

char* getenv_r(const char *name,const int buflen){
    int i,len,olen;
    char *envbuf;
    pthread_once(&init_done,thread_init);
    len = strlen(name);
    pthread_mutex_lock(&env_mutex);
    envbuf = (char*)pthread_getspecific(key);
    if(envbuf == NULL){
        envbuf = (char*)malloc(100);
        if(envbuf == NULL){
            pthread_mutex_unlock(&env_mutex);
            return;
        }
        pthread_setspecific(key,envbuf);
    }
    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;
	    }
            strcpy(envbuf,&environ[i][len+1]);//取地址複製
	    pthread_mutex_unlock(&env_mutex);
	    return envbuf;
	}
    }
    pthread_mutex_unlock(&env_mutex);
    return;
}
void* thread_func1(void* arg){
    //char buf[100];
    //getenv_r("HOME",buf,100);
    printf("HOME=%s\n",getenv_r("HOME",100));
    pthread_exit((void*)1);
}
void* thread_func2(void* arg){
    //char buf[100];
    //getenv_r("SHELL",buf,100);
    printf("SHELL=%s\n",getenv_r("SHELL",100));
    pthread_exit((void*)2);
}

int main(){
    pthread_t tid1,tid2;
    void* ret;
    pthread_create(&tid1,NULL,thread_func1,NULL);
    pthread_create(&tid2,NULL,thread_func2,NULL);
    pthread_join(tid1,&ret);
    pthread_join(tid2,&ret);
    exit(0);
}

6、取消選項

取消選項包括可取消狀態和可取消類型,針對線程在響應pthread_cancel函數調用時候所呈現的行爲。
可取消狀態取值爲:PTHREAD_CANCLE_ENABLE (默認的可取消狀態)或PTHREAD_CANCLE_DISABLE。
取消類型也稱爲延遲取消,類型可以爲:PTHREAD_CANCLE_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。
通過下面函數可設置取消狀態和取消類型:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void); 

7、線程和信號
每個線程都有自己的信號屏蔽字,但是信號的處理是進程中所有線程共享的。這意味着儘管單個線程可以阻止某些信號,但當線程修改了與某個信號相關的處理行爲以後,所有的線程都必須共享這個處理行爲的改變。這樣如果一個線程選擇忽略某個信號,而其他的線程可以恢復信號的默認處理行爲,或者爲信號設置一個新的處理程序,從而可以撤銷上述線程的信號選擇。
線程中使用pthread_sigmask來等同於進程中使用sigprocmask來阻止信號發送。
#include <signal.h>
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oset);
線程中可以通過調用sigwait等待一個或多個信號發生。
int sigwait(const sigset_t *set, int *signop)
set參數指出了線程等待的信號集,signop指向的整數將作爲返回值,表明發生信號的數量。
要把信號發送到進程可以調用kill函數
kill(getpid(),signo)
要把信號發送到線程,可以調用pthread_kill
int pthread_kill(pthread_t threadid,int signo)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

int quitflag = 0;
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;

void* thread_func(void* arg){
    int signo;
    puts("No thread func first output");
    while(1){
        sigwait(&mask,&signo);
        switch(signo){
	    case SIGINT:
		printf("Interrupt\n");break;
	    case SIGQUIT:
		pthread_mutex_lock(&lock);
		quitflag = 1;
		pthread_mutex_unlock(&lock);
	        pthread_cond_signal(&wait);
	    default: exit(1);
	}
    }
}

int main(){
    sigset_t omask;
    pthread_t tid;
    sigemptyset(&mask);
    sigaddset(&mask,SIGINT);
    sigaddset(&mask,SIGQUIT);
    pthread_sigmask(SIG_BLOCK,&mask,&omask);
    pthread_create(&tid,NULL,thread_func,NULL);
    puts("I'm first output, main");
    pthread_mutex_lock(&lock);//很不理解這裏加鎖爲什麼不會出現死鎖
    puts("lock");
    while(quitflag == 0)
	pthread_cond_wait(&wait,&lock);
    pthread_mutex_unlock(&lock);
    puts("unlock");
    quitflag = 0;
    pthread_sigmask(SIG_SETMASK,&omask,NULL);
    sleep(2);
    exit(0);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章