使用pthread庫進行多執行緒程式設計2 - UNIX高階環境程式設計第12章讀書筆記

ATField發表於2007-03-13

12 Thread Control

1 Thread Limits

sysconf函式可以獲得和thread相關的一些系統資訊,主要是執行緒相關的一些最大值:

NAME

Description

Argument

PTHREAD_DESTRUCTOR_ITERATIONS

最大嘗試銷燬執行緒相關資料(Thread Specific Data)的次數,見下面關於Thread-Specific Data的內容

_SC_THREAD_DESTRUCTOR_ITERATIONS

PTHREAD_KEYS_MAX

一個程式所能夠建立的最大鍵數

_SC_THREAD_KEYS_MAX

PTHREAD_STACK_MIN

執行緒棧的最小值

_SC_THREAD_STACK_MIN

PTHREAD_THREADS_MAX

單個程式中的執行緒個數最大值

_SC_THREAD_THREADS_MAX

 

部分概念在後面會提到。

雖然標準定義了這些常量,不過在很多系統上面可能根本就沒有定義對應的Argument(如_SC_THREAD_DESTRUCTOR_ITERATIONS可能未定義),或者sysconf函式返回錯誤。因此在很多時候這些很難派上用場。

2 Thread Attributes

在前面講到pthread_create等函式的時候,這些函式有一個引數pthread_attr_t。預設情況下可以傳NULL。但是如果想自己定義執行緒的相關屬性的話,應該呼叫pthread_attr_init函式來定義:

#include <pthread.h>

 

int pthread_attr_init(pthread_attr_t *attr);

 

int pthread_attr_destroy(pthread_attr_t *attr);

 

返回0表示正常,出錯時返回錯誤值

pthread_attr_init函式負責初始化pthread_attr_t結構為預設值。pthread_attr_destroy負責釋放在pthread_attr_init函式呼叫時分配的記憶體,同時將pthread_attr的內容置為非法。如果要修改屬性,需要呼叫其他函式來手動設定。

基本的執行緒屬性如下:

Name

Description

detachstate

detached狀態,在前一章中有講述

guardsize

執行緒棧底部的Guard緩衝區的大小

stackadddr

執行緒棧的最低地址

stacksize

執行緒棧的大小

1.     Detached State:一個執行緒如果出於Detached狀態,說明此執行緒在退出的時候可以立刻釋放其資源和對應的結束程式碼,從而無法使用pthread_join。可以用pthread_attr_setdetachedstate函式來設定Detach狀態。傳入PTHREAD_CREATE_DETACHED可以讓執行緒啟動的時候就處於Detached狀態,而傳入PTHREAD_CREATE_JOINABLE則是以通常狀態啟動執行緒

#include <pthread.h>

 

int pthread_attr_getdetachedstate(const pthread_attr_t *restrict attr, int *detachstate);

 

int pthread_attr_setdetachedstate(pthread_attr_t *restrict attr, int detachstate);

 

返回0表示正常,出錯時返回錯誤值

 

2.     GuardSize:線上程棧的末尾有一個比較小的記憶體區域,這個記憶體區域是保護起來的,一旦棧發生overflow,系統立刻就會知道,傳送一個SignalWindows也有類似的功能,只不過是用於自動增長棧的大小)。預設情況下這個大小正好是一個頁=PAGESIZE。甚至可以用函式將該數值設定為0來禁止這個功能。如果我們修改了棧地址的話,系統會認為我們會自己處理Overflow的問題,因此也不會提供這個功能。呼叫pthread_attr_get_guardsize & pthread_attr_set_guardsize可以獲得/設定這個值:

#include <pthread.h>

 

int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);

 

int pthread_attr_setguardsize(pthread_attr_t *restrict attr, size_t guardsize);

 

返回0表示正常,出錯時返回錯誤值

 

3.     StackSize:執行緒可以自己設定棧的大小,用pthread_attr_getstacksizepthread_attr_setstacksize

#include <pthread.h>

 

int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);

 

int pthread_attr_setguardsize(pthread_attr_t *restrict attr, size_t stacksize);

 

返回0表示正常,出錯時返回錯誤值

 

4.     StackAddr:當程式中執行緒過多的時候,有可能會棧空間不足。一個方案是用malloc或者nmap來分配新的記憶體,作為一個另外的棧,供執行緒使用。可以呼叫pthread_attr_setstackpthread_attr_getstack來獲得/設定:

#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 *restrict attr, void *stackaddr, size_t *stacksize)

 

返回0表示正常,出錯時返回錯誤值

 

除此之外,還有其他一些執行緒屬性:

1.     Cancellability State

2.     Cancellability Type

3.     Concurrency Level

12會在第6節中講述。

Concurrency Level定義了使用者模式執行緒和核心執行緒/程式之間的對應關係。如果具體作業系統實現是按照11,也就是一個使用者模式執行緒對應一個核心模式執行緒的話,那麼修改這個值沒有作用。但是如果作業系統實現用少量核心模式執行緒/程式來模擬使用者模式執行緒的話,那麼修改這個值可能會提高或者降低程式和系統的效能。Level值並沒有具體的意義,只是一個hintLevel=0表示讓系統自動選擇。函式原型如下:

#include <pthread.h>

 

int pthread_attr_getconcurrency(void);

 

int pthread_attr_setconcurrency(int level);

 

返回0表示正常,出錯時返回錯誤值

注意這個屬性不是和具體執行緒相關的,而是系統級別的。

3 Synchronization Attributes

同步物件也有他們自己的Attributes

3.1 Mutex Attributes

Mutex的屬性型別為pthread_mutexattr_t。可以用pthread_mutexattr_initpthread_mutexattr_destroy來建立和釋放Mutex Attributes。類似的,pthread_mutexattr_init函式會將結構初始化為預設值。

#include <pthread.h>

 

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

 

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

 

返回0表示正常,出錯時返回錯誤值

 

Mutex的屬性有:

1.     Process-Shared:指定Mutex是否為多個程式所共享。預設值是PTHREAD_PROCESS_PRIVATE,即只有建立者程式才可以訪問此Mutex。也可以設定為PTHREAD_PROCESS_SHARED,在多個程式之間共享。

#include <pthread.h>

 

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);

 

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

 

返回0表示正常,出錯時返回錯誤值

 

2.     Type:指定Mutex的型別。Mutex有下列型別:

Type

未釋放鎖的情況獲得鎖

未獲得鎖情況下釋放鎖

已釋放鎖的情況下再次釋放

Description

PTHREAD_MUTEX_NORMAL

死鎖

未定義

未定義

一般的Mutex

PTHREAD_MUTEX_ERRORCHECK

出錯

出錯

出錯

加強錯誤檢查

PTHREAD_MUTEX_RECURSIVE

允許

出錯

出錯

允許單個執行緒獲得鎖多次,需要多次釋放,但是不能超過獲得鎖的次數。一般用來處理可重入的函式,見下面一章

PTHREAD_MUTEX_DEFAULT

未定義

未定義

未定義

完全沒有錯誤檢查

通過呼叫pthread_mutexattr_gettype & pthread_mutexattr_settype來獲得/設定對應的type

#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);

 

返回0表示正常,出錯時返回錯誤值

 

3.2 Reader-Writer Lock Attributes

類似Mutex Attributes,但是隻支援Process Shared屬性。

3.3 Condition Variable Attributes

類似Mutex Attributes,但是隻支援Process Shared屬性。

4 Reentrancy

1.     大部分Single UNIX Specification所定義的函式都是執行緒安全的,但是也有不少例外。實際使用的時候建議參考文件,確定函式是否是執行緒安全。

2.     檔案支援用ftrylockfile, flockfile, funlockfile來鎖定檔案訪問。標準IO函式被要求必須呼叫在內部實現中呼叫flockfile, funlockfile。基於字元的部分IO函式具有非執行緒安全版本,以_unlocked結尾,如:getchar_unlocked, getc_unlocked, putchar_unlocked, putc_unlocked

3.     書中提供了一個可重入的getenv_r實現。要點是:

a.     用到了Recursive Mutex(使用pthread_mutexattr_settype函式呼叫設定)來保護自己和其他執行緒衝突(普通的Mutex就可以做到),同時允許重入(必須用Recursive Mutex

b.     要求呼叫者提供自己的buffer,而不是用靜態全域性變數envbuf來訪問結果

c.     使用pthread_once函式保證只呼叫一個初始化函式一遍,用於初始化Mutex(當然用其他方法也可以)

5 Thread-Specific Data

1.     Thread-Specific Data是一種很方便的將資料和執行緒聯絡起來的方法,在C Runtime中也大量用到Thread-Specific Data來維護執行緒相關的資料,一個典型的例子是errno:實際上errno是一個函式呼叫,返回和執行緒相關的錯誤值。Windows中有類似的機制,稱為TLS (Thread Local Storage)

2.     訪問Thread-Specific Data需要使用Key。不同執行緒使用同一個key訪問同一型別的資料(比如Errno),但是可以存放不同的值。Key的型別為pthread_key_t

3.     pthread_key_create函式建立key

#include <pthread.h>

 

int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *))

 

返回0表示正常,出錯時返回錯誤值

建立好之後key對應的Thread-Specific DataNULL

Destructor函式指標指定當pthread_key_t被刪除的時候需要自動呼叫的函式,可以傳NULL。引數值為TSD的具體值,必然非NULL。線上程正常退出時候,如return或者pthread_exit,當資料值為非NULL時候會呼叫。但是當執行緒非正常退出,如呼叫exit, _exit,  _Exit, abort或者其他非正常退出的時候,destructor不會被呼叫。一般情況下,這個destructor用來銷燬使用者用mallocThread-Specific Data分配的空間。注意:一般不應該用destructor來呼叫pthread_key_delete,因為delete對於一個key只用調一次,而destructor是對每個執行緒都呼叫的,前提是執行緒正常退出並且TSD不為NULL

Key的總數量可能會有限制。可以用PTHREAD_KEYS_MAX來查詢最大值。

因為呼叫Destructor的時候這個Destructor可能又會建立新的Key,所以當執行緒退出的時候,呼叫Destructor的過程會反覆繼續直到沒有key具有非NULL值或者次數到達最大值PTHREAD_DESTRUCTOR_ITERATIONS為止。這個值可以用sysconf獲得。

4.     pthread_key_delete函式刪除key

#include <pthread.h>

 

int pthread_key_delete(pthread_key_t *keyp)

 

返回0表示正常,出錯時返回錯誤值

 

注意,呼叫此函式不會導致Destructor被呼叫!

5.     可以用pthread_once函式保證某個函式只被調一次,用法如下:

#include <pthread.h>

 

pthread_once_t initflag = PTHREAD_ONCE_INIT;

 

int pthread_once(pthread_once_t *initflag, void (*initfn)(void));

 

返回0表示正常,出錯時返回錯誤值

即使在多個執行緒中被調,pthread_once保證initfn在程式中只會呼叫一次。

6.     pthread_getspecificpthread_setspecific用於訪問key所對應的Thread-Specific Data,就是一個void *指標:

#include <pthread.h>

 

Void * pthread_getspecific(pthread_key_t key)

 

返回和key相關的當前執行緒的Thread-Specific Data

 

int pthread_setspecific(pthread_key_t key, const void *value)

 

返回0表示正常,出錯時返回錯誤值

 

6 Cancel Options

除了上面介紹的Thread Attributes之外,還有兩個Thread Attributes沒有介紹,這兩個均和pthread_cancel函式有關。

1.     Cancelability State:表示允許或者禁止pthread_cancel呼叫。PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE分別對應允許和禁止(預設情況下自然是允許)。呼叫pthread_setcancelstate來設定狀態:

#include <pthread.h>

 

int pthread_setcancelstate(int state, int *oldstate)

 

返回0表示正常,出錯時返回錯誤值

注意:這個函式對當前執行緒有效,並非設定pthread_attr_t

如果對一個執行緒呼叫pthread_cancel,執行緒會繼續執行知道執行緒到達一個Cancellation Point,也就是可撤銷點。POSIX.1定義了一些可以作為Cancellation Point的一些函式。

 

你也可以自己定義自己的Cancellation Point,通過呼叫pthread_testcancel

#include <pthread.h>

 

int pthread_testcancel(void)

 

返回0表示正常,出錯時返回錯誤值

 

2.     Cancelability Type:指定Cancel的型別。預設情況下,行為正如我們之前所描述的那樣,執行緒會執行到一個Cancellation PointCancel,稱之為Deferred Cancellation,對應的常量為PTHREAD_CANCEL_DEFERRED。此外,還支援一種稱為PTHREAD_CANCEL_ASYNCHRONOUS的型別。這種情況下,執行緒會立刻被Cancel,無需得到Cancellation Point。使用pthread_setcanceltype可以改變執行緒的CancelType屬性屬性

#include <pthread.h>

 

int pthread_setcanceltype(int type, int *oldtype)

 

返回0表示正常,出錯時返回錯誤值

注意:這個函式對當前執行緒有效,並非設定pthread_attr_t

 

7 Threads and Signals

pthread_sigmask函式可以阻止Signal的傳送:

#include <pthread.h>

 

int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)

 

返回0表示正常,出錯時返回錯誤值

 

呼叫sigwait函式可以等待signal的產生:

#include <pthread.h>

 

int sigwait(const sigset_t *restrict set, int *restrict signop);

 

返回0表示正常,出錯時返回錯誤值

signalpending的情況下,呼叫sigwait會立刻返回並且把signalpending list中移走,這樣這個signal就不會被呼叫。為了避免這種行為,可以將pthread_sigmasksigwait合用,首先用pthread_sigmasksignal產生之前阻止某個signal,然後用sigwait等待這個signalSigwait會自動Unblock這個signal,然後在等待結束之後恢復mask

呼叫pthread_kill可以給一個執行緒傳送signal

#include <pthread.h>

 

int pthread_kill(pthread_t thread, int signo)

 

返回0表示正常,出錯時返回錯誤值

 

8 Threads and fork

當執行緒呼叫fork的時候,整個程式的地址空間都被copy(嚴格來說是copy-on-write)到child。所有Mutex  / Reader-Writer Lock / Condition Variable的狀態都被繼承下來。子程式中,只存在一個執行緒,就是當初呼叫fork的程式的拷貝。由於不是所有執行緒都被copy,因此需要將所有的同步物件的狀態進行處理。(如果是呼叫exec函式的話沒有這個問題,因為整個地址空間被丟棄了)處理的函式是pthread_atfork

#include <pthread.h>

 

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

 

返回0表示正常,出錯時返回錯誤值

 

1.     Prepare:在fork建立child程式之前,在parent程式中呼叫。職責是:獲得所有的鎖

2.     Parent:在fork建立child程式之後,但在fork呼叫返回之前,在parent程式中呼叫。職責是:釋放在prepare中獲得的所有的鎖

3.     Child:在fork建立child程式之後,在fork呼叫返回值錢,在child程式中呼叫。職責是:釋放在prepare中獲得的所有的鎖。看起來childParent這兩個handler做的是重複的工作,不過實際情況不是這樣。由於forkmake一份程式地址空間的copy,所以parentchild是在釋放各自的鎖的copy

 

9 Threads and I/O

因為檔案指標的位置是和程式相關的,所以當不同執行緒呼叫lseekreadwrite的時候容易造成問題。pread & pwrite函式可以用來解決這個問題。這兩個函式會設定檔案指標然後讀寫,作為一個原子操作。

 

 

相關文章