1. 程式人生 > 實用技巧 >《UNIX環境高階程式設計》(APUE) 筆記第十二章 - 執行緒控制

《UNIX環境高階程式設計》(APUE) 筆記第十二章 - 執行緒控制

12 - 執行緒控制

GitHub 地址


1. 執行緒限制

下圖為與 執行緒操作 有關的一些 限制

可以通過 sysconf 函式進行查詢 。

2. 執行緒屬性

可使用 pthread_attr_t 結構修改執行緒預設屬性,並把這些屬性與建立的執行緒聯絡起來。

初始化反初始化

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthead_attr_t *attr);
//返回值:若成功,返回0;否則,返回錯誤編號

POSIX.1 定義的執行緒屬性有:

2.1 分離狀態屬性

如果不需要了解執行緒的終止狀態,可以修改 detachstate 屬性,讓執行緒一開始就處於 分離狀態 ,且讓作業系統線上程退出時收回它所佔的資源:

#include <pthread.h>
int pthread_atr_getdetachstate(const pthread_attr_t *restrict attr,
int *detachstate);
int pthread_attr_stdetachstate(pthread_attr_t *attr, int *detachstate);
//返回值:若成功,返回0;否則,返回錯誤編號
//detachstate引數:PTHREAD_CREATE_DETACHED表示以分離狀態啟動執行緒
//PTHREAD_CREATE_JOINABLE表示正常啟動執行緒,應用程式可以獲取執行緒的終止狀態

2.2 執行緒棧屬性

可以使用 pthread_attr_getstackpthread_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);
//返回值:若成功,返回0;否則,返回錯誤編號

如果執行緒棧的虛地址空間都用完了,可以使用 malloc 或者 mmap 來為可替代的棧分配空間,並用 pthread_attr_setstack 函式來改變新建執行緒的棧位置。\(stackaddr\) 引數指定的地址可以用作執行緒棧的記憶體範圍中的最低可定址地址。

2.3 棧的最小長度屬性

如果希望改變預設的棧大小,又不想自己處理執行緒棧的分配問題,可通過 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);
//返回值:若成功,返回0;否則,返回錯誤編號

選擇的 \(stacksize\) 不能小於 PTHREAD_STACK_MIN

需要調整棧大小的 原因

  • 需要 調小 的原因:對於shuxing執行緒來說,一個程式中的虛地址空間必須被所有的執行緒棧共享,如果執行緒很多,以致於執行緒棧的累計大小超過了可用的虛地址空間,就需要減少預設的執行緒棧大小
  • 需要 調大 的原因:如果執行緒呼叫的函式分配了大量的自動變數,或者呼叫的函式設計許多很深的 棧幀 ,那麼需要的棧的大小可能要不預設的大

2.4 棧的警戒緩衝區屬性

執行緒屬性 \(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 *attr, size_t guardsize);
//返回值:若成功,返回0;否則,返回錯誤編號

如果修改了執行緒屬性 \(stackaddr\) ,系統認為我們將自己管理棧,進而使棧警戒緩衝區機制無效,這等同於把 \(guardsize\) 執行緒屬性設為 \(0\) 。

如果 \(guardsize\) 執行緒屬性被修改了,作業系統可能會把它取為頁大小的整數倍。如果執行緒指標溢位到警戒區域,應用程式就可能通過訊號接收到出錯資訊。

3. 互斥量屬性

互斥量屬性使用 pthread_mutexattr_t 表示。

初始化反初始化

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
//返回值:若成功,返回0;否則,返回錯誤編號

3.1 程式共享屬性

程式共享 互斥屬性預設為 PTHREAD_PROCESS_PRIVATE ,此時在程式中,多個執行緒可以訪問一個同步物件。

程式訪問共享資料也需要同步,如果 程式共享 互斥量屬性設定為 PRHREAD_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;否則,返回錯誤編號

3.2 健壯屬性

健壯屬性 與多個程式間共享的互斥量有關,它定義互斥量持有程式終止時,互斥量狀態恢復的問題。

預設值是 PTHREAD_MUTEX_STALLED ,這意味著持有互斥量的程式終止時不需要採取特別的動作,以後對pthread_mutex_lock() 的所有呼叫程式將以不確定的方式被阻塞。

另一個取值是 PTHREAD_MUTEX_ROBUST ,這將導致:若程式獲取鎖,而鎖被另一個程式持有且終止時未解鎖,此時該執行緒會阻塞,從 pthread_mutex_lock 返回的值為 EOWNERDEAD ,應用程式通過此返回值可以知道需要恢復互斥量。

使用 健壯 的互斥量改變了使用 pthread_mutex_lock 的方式,因為必須檢查 \(3\) 個返回值而非 \(2\) 個:不需要恢復的成功、需要恢復的成功以及失敗 。(若不使用健壯的互斥量,只檢查成功和失敗)

#include <pthread.h>
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr,
int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
//返回值:若成功,返回0;否則,返回錯誤編號

如果應用狀態無法恢復,線上程對互斥量解鎖之後,該互斥量將處於永久不可用狀態。為了避免這樣的問題,執行緒可以呼叫 pthread_mutex_consistent 函式,指明與該互斥量相關的狀態在互斥量解鎖之前是一致的:

#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
//返回值:若成功,返回0;否則,返回錯誤編號

執行緒通過 pthread_mutex_consistent 能讓互斥量正常工作,若沒呼叫此函式就對互斥量進行解鎖,那麼其他試圖獲得該互斥量的阻塞執行緒就會得到錯誤碼 ENOTRECOVERABLE ,若發生了此情況,互斥量將不再可用 。

3.3 型別屬性

型別 互斥量屬性控制著互斥量的鎖定特性:

  • 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);
//返回值:若成功,返回0;否則,返回錯誤編號

4. 讀寫鎖屬性

讀寫鎖屬性pthread_rwlockattr_t 表示。

初始化和反初始化:

#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
//返回值:若成功,返回0;否則,返回,返回錯誤編號

讀寫鎖支援的唯一屬性是 程式共享 屬性,它與互斥量的程式共享屬性是相同的:

#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,
int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
//返回值:若成功,返回0;否則,返回錯誤編號

5. 條件變數屬性

條件變數有兩個屬性:程式共享 屬性和 時鐘 屬性。

初始化與反初始化:

#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
//返回值:若成功,返回0;否則,返回錯誤編號

5.1 程式共享屬性

程式共享屬性 控制著條件變數是可以被單程式的多個執行緒使用,還是可以被多程式的執行緒使用

#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
//返回值:若成功,返回0;否則,返回錯誤編號

5.2 時鐘屬性

時鐘 屬性控制計算 pthread_cond_timewait 函式的超時引數( tsptr )時採用的是哪個時鐘。

#include <pthread.h>
int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,
clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id);
//返回值:若成功,返回0;否則,返回錯誤編號

6. 屏障屬性

初始化和反初始化:

#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
//返回值:若成功,返回0;否則,返回錯誤編號

屏障屬性只有 程式共享 屬性,它控制著屏障是可以 被多程式的執行緒使用PTHREAD_PROCESS_SHARED ),還是隻能 被初始化屏障的程式內的多執行緒使用PTHREAD_PROCESS_PRIVATE ):

#include <pthread>
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,
int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int pshared);
//返回值:若成功,返回0;否則,返回錯誤編號

7. 重入

如果一個函式在相同的時間點可以被多個執行緒安全地呼叫(一個函式對多個執行緒來說是 可重入的 ),就稱該函式是 執行緒安全 的 。

作業系統實現支援執行緒安全函式這個特性時,對 POSIX.1 中的一些非執行緒安全函式會提供可替代的 執行緒安全版本:在非執行緒安全版本名字後加 _r

很多函式並不是執行緒安全的,因為它們返回的資料存放在 靜態的記憶體緩衝區 中,通過修改介面,要求呼叫者自己 提供緩衝區 可以使函式變為執行緒安全。

8. 執行緒特定資料

執行緒特定資料(執行緒私有資料) 是儲存和查詢某個特定執行緒相關資料的一種機制,使用執行緒特定資料可以使每個執行緒訪問它自己單獨的資料副本,而不需要擔心與其他執行緒的同步訪問問題。

使用執行緒特定資料的 原因

  • 有時候需要維護基於每執行緒的資料
  • 它提供了讓基於程式的介面適應多執行緒環境的機,如 errno 被定義為執行緒私有資料,這樣,一個執行緒做了重置 errno 的操作也不會影響程式中其他執行緒的 errno

在分配執行緒特定資料之前,需要建立與該資料關聯的 ,這個鍵用於獲取對執行緒特定資料的訪問:

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
//返回值:若成功,返回0;否則,返回錯誤編號

建立的鍵儲存在 \(keyp\) 指向的記憶體單元中,這個鍵可被程式中所有執行緒使用,但每個執行緒把這個鍵與不同的執行緒特定資料地址進行關聯。建立新鍵時,每個執行緒的資料地址設為空值。

\(destructor\) 為與該鍵關聯的解構函式。當執行緒呼叫 pthread_exit 、執行緒執行返回或執行緒被取消時,若資料地址已被置為非空值,那麼解構函式就會被呼叫,它唯一的引數就是該資料地址。如果執行緒呼叫了 exit_exit_Exitabort ,或者出現其他非正常推出時,就不會呼叫解構函式。

執行緒通常使用 malloc 為執行緒特定資料分配記憶體。

執行緒呼叫 pthread_key_delete 來取消與執行緒特定資料值之間的關聯關係:

#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
//返回值:若成功,返回0;否則,返回錯誤編號

pthread_once 用於某個多執行緒呼叫的模組使用前的初始化,但是無法判定哪個執行緒先執行,從而不知道把初始化程式碼放在哪個執行緒合適的問題:

#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
//返回值:若成功,返回0;否則,返回錯誤編號

\(initflag\) 必須是一個非本地變數(如全域性變數或靜態變數),而且必須初始化為 PTHREAD_ONCE_INIT

如果每個執行緒都呼叫 pthread_once ,系統就能保證初始化例程 \(initfn\) 只被呼叫一次,即系統首次呼叫 pthread_once 時 。

鍵一旦建立後,就可以通過呼叫 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);
//返回值:若成功,返回0;否則,返回錯誤編號

9. 取消選項

可取消狀態可取消型別 這兩個執行緒屬性沒有包含在 pthread_attr_t 結構中,它們影響著響應 pthread_cancel 函式呼叫時所呈現的行為 。

執行緒可以通過 pthread_setcancelstate 修改它的 可取消狀態

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
//返回值:若成功,返回0;否則,返回錯誤編號
//state: PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DISABLE

pthread_setcancelstate 把當前的可取消狀態設定為 \(state\) ,把原來的可取消狀態儲存在 \(oldstate\) 指向的記憶體單元,這兩步是一個原子操作。

pthread_cancel 呼叫並不等待執行緒終止。預設情況下,執行緒在取消請求發出以後還是繼續執行,直到某個 取消點。取消點是執行緒檢查它是否被取消的一個位置,如果取消了,則按照請求行事。

執行緒啟動時預設的可取消狀態是 PTHREAD_CANCEL_ENABLE 。當狀態設為 PTHREAD_CANCEL_DISABLE 時,對 pthread_cancel 的呼叫並不會殺死執行緒。相反,取消請求對這個執行緒來說還處於掛起狀態,當取消狀態再次變為PTHREAD_CANCEL_ENABLE 時,執行緒將在下一個取消點上對所有掛起的取消請求進行處理 。

POSIX.1 定義了一些函式作為 取消點 ,也可以呼叫 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);
//返回值:若成功,返回0;否則,返回錯誤編號
```![](./img/12-374.png) 此函式把 **取消型別** 設定為 $type$ 。引數型別可以是 `PTHREADCANCEL_DEFFERED` ( 延遲取消 )和 `PTHREAD_CANCEL_ASYNCHRONOUS` ( 非同步取消 )。把原來的取消型別返回到 $oldtype$ 中。 使用 **非同步取消** ,執行緒可以在任意時間撤銷,不是非得遇到取消定才能被取消。 ## 10. 執行緒和訊號 每個執行緒都有自己的 **訊號遮蔽字** ,但是訊號的處理是程式中 **所有執行緒共享** 的。這意味著單個執行緒可以阻止某些訊號,但當某個執行緒修改了與某個給定訊號相關的處理行為以後,所有的執行緒都必須共享這個處理行為的改變。 程式中的訊號是遞送到 **單個執行緒** 的,如果一個訊號與硬體故障相關,那麼該訊號一般會被髮送到引起該事件的執行緒中去,而其他的訊號則被髮送到任意一個執行緒。 **sigpromask** 的行為在多執行緒的程式中沒有定義,執行緒必須使用 **pthread_sigmask** 來阻止訊號傳送: ```c++
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//返回值:若成功,返回0;否則,返回錯誤編號

\(set\) 引數包含執行緒用於修改訊號遮蔽字的訊號集。

\(how\) 引數可以為:

  • SIG_BLOCK:把訊號集新增到訊號遮蔽字中
  • SIG_SETMASK:用訊號集替換執行緒的訊號遮蔽字
  • SIG_UNBLOCK:從執行緒訊號遮蔽字中移除訊號集

如果 \(oset\) 引數不為空,執行緒之前的訊號遮蔽字就儲存在它指向的 sigset_t 結構中。

執行緒可以通過把 \(set\) 引數設定為 NULL ,並把 \(oset\) 引數設定為 sigset_t 結構的地址,來 獲取當前的訊號遮蔽字 。此時 \(how\) 引數被忽略 。

執行緒通過呼叫 sigwait 等待一個或多個訊號的出現

#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
//返回值:若成功,返回0;否則,返回錯誤編號

\(set\) 引數指定了執行緒等待的訊號集。返回時,\(signop\) 指向的整數將包含傳送訊號的數量。

為避免錯誤行為發生,執行緒在呼叫 sigwait 之前,必須阻塞那些它正在等待的訊號。sigwait 函式會原子地取消訊號集的阻塞狀態,直到有新的訊號被遞送。在返回之前,sigwait 將恢復執行緒的訊號遮蔽字。

使用 sigwait 的好處在於它可以 簡化訊號處理 。允許把非同步產生的訊號用同步的方式處理。為 防止訊號中斷執行緒 ,可以把訊號加到每個執行緒的訊號遮蔽字中,然後安排專用執行緒處理訊號。

傳送訊號給執行緒 ,呼叫 pthread_kill

#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
//返回值:若成功,返回0;否則,返回錯誤編號

可以傳一個 \(0\) 值的 \(signo\) 來檢查執行緒是否存在。若果訊號的預設處理動作是終止該程式,那麼把訊號傳遞給某個執行緒仍然會殺死整個程式 。

11. 執行緒和 fork

子程式通過繼承父程式整個地址空間的副本,還從父程式那裡繼承了每個 互斥量、讀寫鎖和條件變數的狀態 。如果父程式包含一個以上的執行緒,子程式在 fork 返回以後,如果緊接著不是馬上呼叫 exec 的話,就需要 清理鎖狀態

清除鎖狀態 ,可以通過呼叫 pthread_atfork 函式建立 fork 處理程式:

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
//返回值:若成功,返回0;否則,返回錯誤編號

此函式可以安裝 \(3\) 個 fork 處理程式

  • \(prepare\) fork 處理程式 由父程式建立子程式前( 即呼叫 fork 後 )呼叫。其任務是獲取父程式定義的所有鎖
  • \(parent\) fork 處理程式 是在 fork 建立子程式後、返回之前在 父程式上下文 中呼叫的。其任務是對 \(prepare\) fork 處理程式 獲得的所有鎖進行解鎖
  • \(child\) fork 處理程式fork 返回之前在 子程式上下文 中呼叫。其任務也是對 \(prepare\) fork 處理程式 獲得的所有鎖進行解鎖

可以多次呼叫 pthread_atfork 函式從而設定 多套 fork 處理程式 。如果不需要使用其中某個處理程式,可以給特定的處理程式引數傳入空指標,它就不會起任何作用了。

使用多個 fork 處理程式 時,處理程式的呼叫順序並不相同。\(parent\) 和 \(child\) fork 處理程式 是以它們註冊時的順序進行呼叫的,而 \(prepare\) fork 處理程式 的呼叫順序與它們註冊時的順序相反。這樣可以允許多個模組註冊它們自己的 fork 處理程式,而且可以保持鎖的層次 。

12. 執行緒和 I/O

程式中所有執行緒共享相同的 檔案描述符

使用 pread 可以使偏移量的設定和資料的讀取稱為一個原子操作。

使用 pwrite 可以解決併發執行緒對同一檔案進行寫操作的問題。