[單刷APUE系列]第十二章——執行緒控制

山河永寂發表於2019-05-10

執行緒屬性

在前一章中,都是使用的函式預設的屬性來賦予執行緒,但是pthread允許我們通過設定物件關聯的不同屬性來細調執行緒和同步物件的行為。而管理這些屬性的函式基本都是形式相同的。

  1. 執行緒和執行緒屬性關聯、互斥量和互斥量屬性關聯,一個屬性物件可以代表多個屬性

  2. 有一個初始化函式,並且可以將屬性設定為預設值

  3. 有一個解構函式,能夠銷燬屬性物件並且回收資源

  4. 每個屬性都有一個從屬性物件中獲取屬性值的函式

  5. 每個屬性都有一個設定屬性值的函式

還記得我們在上一章中使用pthread_create函式的時候,傳入的是null指標,函式就會使用預設值來設定執行緒,但是我們也可以使用pthread_attr_t結構體修改執行緒預設屬性。

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

就和之前的執行緒構造解構函式一樣,只不過產生的是執行緒屬性物件而已。
在前面內容中講到過分離執行緒的概念,如果我們隊執行緒的終止狀態不感興趣的話,就可以使用分離執行緒讓作業系統來對執行緒進行回收。但是,如果我們是在建立執行緒的時候就需要分離執行緒,這就需要我們使用pthread_attr_setdetachstate函式將其執行緒屬性設定為PTHREAD_CREATE_DETACHED或者PTHREAD_CREATE_JOINABLE兩種值,也就是分離和等待。

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

SUS標準除了POSIX標準規定的以外,還有一些另外的屬性,目前現在的Unix系統基本都是遵循這個標準的,所以執行緒棧也是一個重要屬性。在編譯階段使用_POSIX_THREAD_ATTR_STACKADDR_POSIX_THREAD_ATTR_STACKSIZE符號來檢查是否支援這兩個執行緒棧屬性,當然就像前面講過的一樣,我們也可以使用sysconf函式執行時檢查。一般來說,這編譯時檢查和執行時檢查都是必須的,因為你不知道是否會出現跨平臺編譯。
在蘋果系統平臺下,並沒有發現存在pthread_attr_getstackpthread_attr_setstack函式,蘋果系統將其分拆成了兩個函式,也就是總共四個函式

int pthread_attr_getstackaddr(const pthread_attr_t *restrict attr, void **restrict stackaddr);
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);

int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

程式我們知道是有一個棧的,而且程式的虛擬記憶體地址空間是固定的,所以大小完全無所謂,但是所有執行緒共享著同一個程式的地址空間,所以,如果執行緒棧累計大小超過了可用空間,就會導致溢位。
如果執行緒棧的虛地址空間用完了,可以使用malloc或者mmap來分配空間,並且使用上面的函式改變新建執行緒的棧位置,stackaddr引數指定的是棧的最低記憶體地址.
執行緒屬性guardsize控制著執行緒棧末尾後用於避免棧溢位的擴充套件記憶體大小。這個屬性預設值是根據Unix具體實現的。可以把guardsize執行緒屬性設定為0,不允許屬性的這種特徵行為發生,這就會導致警戒緩衝區不存在。同樣,如果修改了stackaddr,系統就會認為,我們將自己管理棧,從而會導致警戒緩衝區無效。但是蘋果系統沒有提供這個函式,應該是預設已經設定了一個預設值,或者說根本就沒有設定警戒緩衝區。

同步屬性

除了執行緒屬性外,執行緒同步物件也有屬性,就比如說各種鎖

互斥量屬性

互斥量屬性使用pthread_mutexattr_t結構體,在前面的章節中,是使用PTHREAD_MUTEX_INITIALIZER常量或者用指向互斥量屬性結構的空指標作為引數呼叫pthread_mutex_init函式。系統提供了相應的介面用於初始化。

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

這兩個函式就是初始化和反初始化函式,其中需要注意的就是:程式共享屬性、健壯屬性、以及型別屬性。
程式中,我們知道,多個執行緒可以訪問同一個資源,但是程式訪問同一個資源就需要設定PTHREAD_PROCESS_PRIVATE,或者說是程式共享互斥量屬性。
Unix環境實際上有一種機制:允許獨立的程式把同一個記憶體資料塊對映到一個公共的地址空間中,然後多個程式就能訪問共享的資料了,如果程式共享互斥量設定為PTHREAD_PROCESS_SHARED,多個程式彼此共享的記憶體資料塊分配額互斥量就可以用於程式同步。

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

互斥量健壯屬性和多個程式間共享的互斥量有關。這意味著,當持有互斥量的地址終止時,需要解決互斥量狀態恢復問題

int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);

健壯性只有兩種情況,PTHREAD_MUTEX_STALLED這意味著程式終止時沒有任何動作會採用,就可能導致等待著這個互斥量的其他程式處於等待狀態。PHTREAD_MUTEX_ROBUST則是會對這個互斥量解鎖。
非常遺憾的是,蘋果沒有對以上兩種型別做出介面和支援,所以我們也只能看看了。
型別互斥量屬性控制著互斥量的鎖定特性。POSIX標準規定了4中型別:

  1. PTHREAD_MUTEX_NORMAL 標準互斥量型別,不對其作任何的錯誤檢查或者死鎖檢測

  2. PTHREAD_MUTEX_ERRORCHECK 提供了錯誤檢查的型別

  3. PTHREAD_MUTEX_RECURSIVE 此型別允許同一執行緒在互斥量解鎖之前對該互斥量進行多次加鎖,並且維護了鎖的計數

  4. PTHREAD_MUTEX_DEFAULT 這個互斥量型別可以提供預設的行為和特性。作業系統在實現它的時候就是對映到其他互斥量的一種

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

上面這兩個函式就是設定和獲取函式

讀寫鎖屬性

讀寫鎖和互斥量非常相似,所以屬性的函式也是基本差不多的,這裡就隨便的列舉一下

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

讀寫鎖唯一支援屬性就是程式共享屬性,他只有兩個可能值。

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

PTHREAD_PROCESS_SHARED Any thread of any process that has access to the memory where the read/write lock resides can manipulate the lock.
PTHREAD_PROCESS_PRIVATE Only threads created within the same process as the thread that initialized the read/write lock can manipulate the lock.  This is the default value.

條件變數屬性

不用多說,先來兩個初始化反初始化函式

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

條件變數只有兩個屬性:程式共享屬性和時鐘屬性,但是好像蘋果沒有這兩個屬性。所以也就不講了。
原著中的屏障屬性,實際上在蘋果系統下沒有說明,所以也就略過了。

重入

由於執行緒是並行執行的,如果在同一時間點呼叫同一個函式,則有可能導致衝突,而如果一個函式在同一時間點可以被多個執行緒安全的呼叫,就稱該函式是執行緒安全的。在Unix系統中,如果函式是執行緒安全的,就會在<unistd.h>中定義符號_POSIX_THREAD_SAFE_FUNCTIONS,當然也可以使用sysconf函式獲取限制。
對於非執行緒安全函式,系統會提供可替代的執行緒安全函式,這些函式只是在名字後面加上_r,表面是可重入函式。但是可重入不代表是非同步安全的,因為訊號處理函式在呼叫的時候可能會導致衝突。

執行緒指定資料

也成為執行緒私有資料,如同字面一樣,執行緒將某些特定資料只允許自身查詢。雖然執行緒模型輕量級的共享資料方式非常方便,但是也有著諸多弊端,所以引入了執行緒私有資料,用於維護基於執行緒的資料。例如errno,程式的errno不能共享給所有執行緒,所以後來就重新重構了errno的實現方式。
但是我們知道,執行緒能訪問程式所有地址空間,除了核心提供的暫存器等儲存,執行緒理論上來說不存在真正私有的執行緒資料。但是有一種機制約束也更加安全些。

int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));

是不是很像鍵值對,沒錯,這就是鍵值對,建立的鍵儲存在keyp指向的記憶體單元中,鍵可以被所有執行緒使用,但是每個執行緒把這個鍵和不同的執行緒特定資料地址關聯。除了鍵以外還有一個解構函式,當執行緒退出時候,如果資料地址被置為非空值,那麼解構函式就會被呼叫,我們可以看到,它就一個無型別指標。
執行緒通常使用malloc為私有資料分配記憶體,解構函式就是釋放已經分配的記憶體,如果不做析構,則會導致記憶體洩露。
執行緒退出時,執行緒特定資料的解構函式將會按照順序被呼叫,當所有解構函式結束後,系統會檢查是否還有非空執行緒特定資料與鍵關聯,如果有,則再次呼叫解構函式。直到所有鍵為空。當然,也有最大嘗試次數。

int pthread_key_delete(pthread_key_t key);

這個函式就是取消鍵和執行緒私有資料的關聯。

取消選項

除了上面的屬性外,實際上還有兩個執行緒屬性:可取消狀態和可取消型別,這兩個屬性影響pthread_cancel函式行為。
可取消狀態屬性有兩個值,PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE,執行緒可以通過呼叫pthread_setcancelstate修改

int pthread_setcancelstate(int state, int *oldstate);

非常容易理解的函式,即可以用來檢視舊狀態也可以用於修改。
在前面章節pthread_cancel呼叫不會等待執行緒終止,而是等到一個取消檢查點,統一檢查狀態,執行緒啟動的時候預設為PTHREAD_CANCEL_ENABLE也就是接收取消,而為PTHREAD_CANCEL_DISABLE則不會殺死執行緒,只會阻塞這個請求,等狀態再次變為接收並且到達下一個檢查點的時候統一處理。
如果一直沒有到達檢查點,可能會導致取消的延遲,所以也提供了一個函式用於生成自己的檢查點。

void pthread_testcancel(void);

在預設情況下,取消是延遲的,但是可以通過呼叫pthread_setcanceltype修改

int pthread_setcanceltype(int type, int *oldtype);

引數型別只有兩種,PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS也就是非同步和延遲。非同步取消時,執行緒可以在任意時間撤銷而不是到檢查點。

執行緒和訊號

在前面關於訊號的章節,訊號是基於程式的,而引入了執行緒之後,訊號的處理就更加複雜了。每個執行緒都有了自己的執行緒遮蔽字,但是訊號的接收處理則是統一給程式的,當程式註冊了訊號處理函式後,所有執行緒的訊號處理都會改變,但是程式中的訊號是傳送給單個執行緒的,一個執行緒可以修改撤銷另一個執行緒的訊號選擇,

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

這就是sigprocmask函式的pthread版本,也就是多執行緒版本。兩者基本相同,不過pthread_sigmask則是工作線上程下。
為了簡化訊號處理,pthread提供了另一個函式

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

set引數指定了執行緒等待的訊號集。返回的時候sig引數指向的記憶體將包含傳送訊號的數量。
這個函式的好處就在於簡化了訊號處理,將非同步產生的訊號通過阻塞的方式同步處理。同樣的,由於執行緒的訊號接收,也有了新的kill函式

int pthread_kill(pthread_t thread, int sig);

If sig is 0, error checking is performed, but no signal is actually sent.sig可以指定為0來測試執行緒的存在。

執行緒和fork

我們講過,當fork的時候,子程式會基本繼承父程式的所有內容,也就是繼承了所有互斥量、讀寫鎖和條件變數。但是由於多執行緒的存在,fork後子程式必須清理鎖的狀態。
也許各位第一反應,是不是父子程式將會同樣是多執行緒,實際上不是這樣的,子程式只會包含父程式呼叫fork的執行緒的副本。由於子程式繼承了鎖,但是卻沒有繼承佔有鎖的執行緒,所以需要清理鎖,但是卻不知道清理哪些。
實際上,POSIX.1規定,在fork和第一個exec之間,子程式只能呼叫非同步訊號安全的函式,也就是限制了子程式“做什麼”,而不是“如何清理”。

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

為了確保清理鎖,程式可以呼叫pthread_atfork函式註冊清理函式,parent函式是fork建立子程式之後、返回之前在父程式中呼叫的,這是對所有鎖進行解鎖。child函式則是在fork返回之前,在子程式中呼叫。實際上兩者解鎖同樣的內容,但是在不同的程式中而已。

執行緒和I/O

多執行緒下所有執行緒共享同樣的描述符,所以需要新的IO函式,而最簡單的辦法就是將其原子操作化,這樣就不會出現IO的衝突。也就是pread和pwrite函式。這裡不再贅述。

相關文章