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

BrianLeeLXT 發表於 2020-06-30

11 - 執行緒

Github 地址


1. 執行緒概念

典型的 UNIX程式 可以看成只有一個 控制執行緒 :一個程式在某一時刻只能做一件事情。有了 多個控制執行緒 ,就可以把程式設計成在某一時刻能夠做不止一件事,每個執行緒處理各自獨立的任務。

每個執行緒都包含有表示執行環境所必需的資訊

  • 程式中表示執行緒的 執行緒 ID
  • 一組暫存器值
  • 排程優先順序和策略
  • 訊號遮蔽字
  • errno 變數
  • 執行緒私有資料

一個程式所有資訊對該程式的所有執行緒都是共享的

  • 可執行程式的程式碼
  • 程式的全域性記憶體和堆記憶體
  • 檔案描述符

執行緒的優點

  • 每個執行緒在進行事件處理時可以採用 同步編成模式 ,同步程式設計模式要比非同步程式設計模式簡單得多
  • 多個程式必需使用作業系統提供的複雜機制才能實現記憶體和檔案描述符的共享。而多個執行緒自動地可以訪問相同的儲存地址空間和檔案描述符
  • 有些問題可以分解從而提高整個程式的吞吐量。有多個執行緒時,相互獨立的任務可以交叉進行(任務的處理過程互不依賴),此時只需要為每個任務分配一個單獨的執行緒
  • 互動的程式同樣可以通過使用多執行緒來改善響應時間,多執行緒可以把程式中處理使用者輸入輸出的部分與其他部分分開

2. 執行緒 ID

每個執行緒都有一個 執行緒 ID ,執行緒 ID 只在它所屬的程式上下文中才有意義。用 pthread_t 資料型別來表示執行緒 ID ,Linux 中使用無符號長整型表示。

pthead_equal 函式對兩個執行緒 ID 進行比較:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//返回值:若想等,返回非0數值;否則,返回0

執行緒可呼叫 pthread_self 函式獲得自身的執行緒ID:

#include <pthread.h>
pthread_t pthread_self(void);

3. 執行緒建立

新增的執行緒通過呼叫 pthread_create 函式建立:

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,
                  void *(*start_rtn)(void *), void *restrict arg);
//返回值:若成功,返回0;否則,返回錯誤編號

pthread_create 成功返回時,新建立執行緒的執行緒 ID通過 \(tidp\) 返回。\(attr\) 引數用於定製各種不同的執行緒屬性,如果為 NULL ,則使用預設屬性。

新建立的執行緒從 \(start\_rtn\) 函式的地址開始執行。若需要向 \(start\_rtn\) 函式傳遞的引數有一個以上,需要把這些引數放到一個結構中,然後把這個結構的地址作為 \(arg\) 引數傳入。

執行緒建立時並不能保證哪個執行緒會先執行,可能是新建立的執行緒,也可能是呼叫執行緒。

4. 執行緒終止

如果 程式中的任意執行緒 呼叫了 exit_Exit 或者 _exit ,那麼整個程式就會終止。如果 訊號處理的預設動作是終止程式 ,那麼傳送到執行緒的訊號就會終止整個程式 。

單個執行緒可以通過 \(3\) 種方式退出(不終止整個程式):

  1. 執行緒可以簡單地從啟動例程中退出,返回值是執行緒的退出碼
  2. 執行緒可以被同一程式中的其他執行緒取消( pthread_cancel
  3. 執行緒呼叫 pthread_exit

pthread_exit 函式用於執行緒退出:

#include <pthread.h>
void pthread_join(pthead_t thread, void *rval_ptr);
//返回值:若成功,返回0;否則返回錯誤編號

pthread_join 用於等待一個執行緒的結束:

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
//返回值:若成功,返回0;否則,返回錯誤編號

pthead_join 的呼叫執行緒將一直阻塞,直到指定的執行緒呼叫 pthread_exit 、從啟動例程中返回或被取消。如果執行緒簡單地從它的啟動例程返回,\(rval\_ptr\) 就包含返回碼。如果執行緒被取消,由 \(rval\_ptr\) 指定的記憶體單元就設定為 PTHREAD_CANCELED 。若將 \(rval\_ptr\) 設定為 NULL ,呼叫 pthread_join 函式可以等待指定的執行緒終止,但並不獲取執行緒的終止狀態。

可以通過 pthread_join 自動把執行緒置於分離狀態,這樣資源就可以恢復。

預設情況下,執行緒的終止狀態會儲存直到對該執行緒呼叫 pthread_join ,如果執行緒已被 分離 ,執行緒的底層儲存資源可以線上程終止時被立即收回。

可以呼叫 pthread_detach 分離執行緒:

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

執行緒可以通過 pthread_cancel 函式來請求 取消同一程式中的其他執行緒

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

預設情況下,pthread_cancel 函式會使得 \(tid\) 標識的執行緒的行為表現為如同呼叫了引數為 PTHREAD_CANCELEDpthread_exit 函式,但是,執行緒可以選擇忽略取消或者控制如何被取消。

pthread_cancel 並不等待執行緒終止,它僅僅提出請求。

執行緒可以安排它退出時需要呼叫的 執行緒清理處理程式

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

一個執行緒可以建立多個清理處理程式,處理程式記錄在棧中,也就是說,它們的執行順序與註冊順序相反。pthread_cleanup_pop 函式刪除上次 pthread_cleanup_push 函式建立的清理處理程式。這兩個函式必須在與執行緒相同的作用域中以匹配的對的形式使用。

當執行緒執行以下動作時,清理函式 \(rtn\) 是由 pthread_cleanup_push 函式排程的,呼叫時只有一個引數 \(arg\)

  • 呼叫 pthread_exit
  • 響應取消請求時
  • 用非零 \(execute\) 引數呼叫 pthread_cleanup_pop

可以看到,如果執行緒是通過從它的啟動例程中返回而終止的話,它的清理處理程式就 不會 被呼叫 。

5. 程式和執行緒原語的比較

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

6. 執行緒同步

當一個執行緒可以修改的變數,其他執行緒也可以讀取或修改的時候,需要對執行緒進行 同步 ,確保它們在訪問變數的儲存內容時不會訪問到無效的值。

執行緒使用 ,同一時間只允許一個執行緒訪問該變數。

7. 互斥量

互斥量 (mutex) 從本質上說是一把鎖,在訪問共享資源前對互斥量進行設定( 加鎖 ),在訪問完成後釋放互斥量( 解鎖 )。對互斥量加鎖以後,任何其他試圖再次對互斥量加鎖的執行緒都會被阻塞,直到當前執行緒釋放該互斥鎖。

互斥變數 是用 pthread_mutex_t 資料型別表示的。可以通過將其設定為 PTHREAD_MUTEX_INITIALIZER 進行初始化(只適用於靜態分配的互斥量),也可以通過 pthrsisuoead_mutex_init 函式進行初始化。如果動態分配互斥量(如呼叫 malloc ),在釋放記憶體前需要呼叫 pthread_mutex_destroy

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//兩個函式的返回值:若成功,返回0;否則,返回錯誤編號

\(attr\) 是互斥量屬性,設為 NULL 則使用預設屬性。

使用 pthread_mutex_lock 對互斥量進行加鎖,使用 pthread_mutex_unlock 對互斥量進行解鎖:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthraed_mutex_unlock(pthread_mutex_t *mutex);
//所有函式的返回值:若成功,返回0;否則,返回錯誤編號

如果執行緒不希望被阻塞,可以使用 pthread_mutex_trylock 嘗試對互斥量進行加鎖。如果呼叫此函式時互斥量處於未鎖住狀態,則此函式將鎖住互斥量,不會出現阻塞直接返回 \(0\) ,否則函式不能鎖住互斥量,返回 EBUSY

8. 避免死鎖

死鎖產生的 原因

  • 執行緒對用一個互斥量加鎖兩次,它自身會陷入死鎖狀態
  • 兩個執行緒都在相互請求另一個執行緒擁有的資源

如果需要對多個互斥量加鎖,可以通過 控制互斥量加鎖的順序 來避免死鎖的發生。有時對互斥量排序很困難,可以先釋放佔有的鎖,然後過一段時間再試,這種情況可以使用 pthread_mutex_trylock 介面避免死鎖。(如果已經佔有某些鎖而且 pthread_mutex_trylock 介面返回成功,就可以前進,如果不能獲取鎖,就可以先釋放已經佔有的鎖,做好清理工作,然後過一段時間再重新試 )。

9. 函式 pthread_mutex_timedlock

函式 pthread_mutex_timelock 函式線上程試圖獲取一個已加鎖的互斥量時,指定執行緒阻塞時間。若超過時間值,pthread_mutex_timelock 不會對互斥量進行加鎖,而是返回錯誤碼 ETIMEDOUT

除時間限制外作用同 pthread_mutex_lock

#include <pthread.h>
#include <time.h>
int pthread_mutex_timelock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
//返回值:若成功,返回0;否則,返回錯誤編號

超時指定願意等待的 絕對時間 (指示在時間 \(X\) 之前可以阻塞等待)。超時時間用 timespec 結構來表示,用秒和納秒來描述時間。

10. 讀寫鎖

pthread_rwlock_t讀寫鎖(共享互斥鎖),它比互斥量允許更高的並行性,適合於對資料結構讀的次數遠大於寫的情況。

讀寫鎖有 3 種狀態 :讀模式下加鎖狀態、寫模式下加鎖狀態、不加鎖狀態。一次只有一個執行緒可以佔有寫模式的讀寫鎖,但是多個執行緒可以同時佔有讀模式的讀寫鎖。

對於讀寫鎖的 各個狀態

  • 當讀寫鎖是 寫加鎖 狀態時,在此鎖被解鎖之前,所有試圖對這個鎖加鎖的執行緒都會被阻塞 。
  • 當讀寫鎖是 讀加鎖 狀態時,所有試圖以讀模式對它進行加鎖的執行緒都可以得到訪問權,但是任何希望以寫模式對此鎖進行加鎖的執行緒都會阻塞。但是當一個執行緒試圖以寫模式獲取鎖時,讀寫鎖會阻塞隨後的讀模式鎖請求,避免讀模式鎖長期佔用 。

讀寫鎖通過 pthread_rwlock_init 函式進行初始化:

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                       const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//兩個函式的返回值:若成功,返回0;否則,返回錯誤編號

也可使用 PTHREAD_RWLOCK_INITIALIZER 常量對靜態分配的讀寫鎖進行初始化。

要在讀模式下鎖定讀寫鎖,需要呼叫 pthread_rwlock_rdlock ;要在寫模式下鎖定讀寫鎖,需要呼叫 pthread_rwlock_wrlock 。不管以何種方式鎖住讀寫鎖,都呼叫 pthread_rwlock_unlock 進行解鎖:

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//返回值:若成功,返回0;否則,返回錯誤編號

讀寫鎖原語的 條件版本

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//返回值:可以獲取鎖時,返回0;否則,返回錯誤EBUSY

帶有 超時 的讀寫鎖:

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr);
//返回值:若成功,返回0;若超時,返回 ETIMEOUT

11. 條件變數

條件變數 是執行緒可用的另一種 同步機制 ,條件變數由互斥量保護,允許執行緒以無競爭的往事等待特定的條件發生 。

條件變數上有兩種 基本操作

  • 等待:一個執行緒因等待條件為真而處於等待在條件變數上,此時執行緒不會佔用互斥量(等待條件前要鎖住互斥量,等待過程中對互斥量解鎖,等待到函式返回(條件改變或超時)後,互斥量再次被鎖住)
  • 通知:另一個執行緒在使條件為真時,通知該條件變數的等待執行緒(在給等待執行緒發訊號時,不需要佔有互斥量)

pthread_cond_t 表示條件變數,初始化 時,可以把常量 PTHREAD_COND_INITIALIZER 賦給靜態分配的條件變數,如果條件變數是動態分配的,則需要呼叫 pthread_cond_init 函式對其進行初始化:

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

使用 pthread_cond_wait 等待條件變數為真:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
                     pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                          pthread_mutex_t *restrict mutex,
                          const struct timespec *restrict tsptr);
//返回值:若成功,返回0;否則,返回錯誤編號

有兩個函式可以用於通知執行緒條件已經滿足。pthread_cond_signal 函式至少能喚醒一個等待該條件的執行緒,而 pthread_cond_broadcast 函式則能喚醒等待該條件的所有執行緒:

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

12. 自旋鎖

自旋鎖 與互斥量類似,但它不是通過休眠使程式阻塞,而是在獲取鎖之前一直處於 忙等 (自旋) 阻塞狀態(不佔用CPU)。當執行緒自旋等待鎖變為可用時,CPU不能做其他事情,這是為什麼自旋鎖只能夠被持有一小段時間的原因。

自旋鎖 可用於:鎖持有時間短,而且執行緒並不希望在重新排程上花費太多的成本。自旋鎖通常作為底層原語用於實現其他型別的鎖。

pthread_spinlock_t 表示自旋鎖,初始化和反初始化函式為:

#include<pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
//返回值:若成功,返回0;否則,返回錯誤編號

可以用 pthread_spin_lockpthread_spin_trylock 函式對自旋鎖進行加鎖,前者在獲取鎖之前一直自旋,後者如果不能獲取鎖,就立即返回 EBUSY 錯誤。呼叫 pthread_spin_unlock 函式解鎖:

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
//返回值:若成功,返回0;否則,返回錯誤編號

13. 屏障

屏障 (barrier) 是使用者協調多個執行緒並行工作的同步機制。屏障允許每個執行緒等待,知道所有的合作執行緒都達到某一點,然後從該點繼續執行。

初始化和反初始化

#include <pthread.h>
int pthread_barrier_init(pthread_barrier *restrict barrier,
                        const pthread_barrierattr_t *restrict attr,
                        unsigned int count);
int pthread_barrier_destroy(pthread_barrier *barrier);
//返回值:若成功,返回0;否則,返回錯誤編號

\(count\) 引數指定在允許所有執行緒繼續執行之前,必須到達屏障的執行緒數目。

使用 pthread_barreir_wait 函式來表明,執行緒已完成工作,準備等所有其他執行緒趕上來:

#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
//返回值:若成功,返回 0 或者 PTHREAD_BARRIER_SERIAL_THREAD;否則,返回錯誤編號

呼叫 pthread_barrier_wait 的執行緒在屏障計數未滿足條件時,會進入休眠狀態。若該執行緒是最後一個呼叫該函式的執行緒,就滿足了屏障計數,所有執行緒都被喚醒。

14. 五個基本同步機制

  • 互斥量
  • 讀寫鎖
  • 條件變數
  • 自旋鎖
  • 屏障