Linux學習總結(八)——線程

進程,它是資源分配的最小單位,進程中的事情需要按照一定的順序逐個進行,那麼:如何讓一個進程中的一些事情同時執行?

這時候就需要使用線程。線程,有時又稱輕量級進程,它是 程序執行的最小單位系統獨立調度和分派 cpu 的基本單位,它是進程中的一個實體。一個進程中可以有多個線程,這些線程共享進程的所有資源,線程本身只包含一點必不可少的資源。

在瞭解線程之前需要清楚幾個概念:

  1. 併發
    併發是指在同一時刻,只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。 看起來同時發生
  2. 並行
    並行是指在同一時刻,有多條指令在多個處理器上同時執行。真正的同時發生
  3. 同步
    彼此有依賴關係的調用不應該“同時發生”,而同步就是要阻止那些“同時發生”的事情。
  4. 異步
    異步的概念和同步相對,任何兩個彼此獨立的操作是異步的,它表明事情獨立的發生。

線程解決了進程的很多弊端:一是由於進程是資源擁有者,創建、撤消與切換存在較大的時空開銷,;二是由於對稱多處理機(SMP)出現,可以滿足多個運行單位,而多個進程並行開銷過大。因此,線程具有以下優勢:

1、在多處理器中開發程序的並行性
2、在等待慢速IO操作時,程序可以執行其他操作,提高併發性
3、模塊化的編程,能更清晰的表達程序中獨立事件的關係,結構清晰
4、佔用較少的系統資源

一、線程的創建:

每個 Linux 都會運行在下面四種狀態下:

  • 就緒
    當線程剛被創建時就處於就緒狀態,或者當線程被解除阻塞以後也會處於就緒狀態。就緒的線程在等待一個可用的處理器,當一個運行的線程被搶佔時,它立刻又回到就緒狀態
  • 運行
    當處理器選中一個就緒的線程執行時,它立刻變成運行狀態
  • 阻塞
    線程會在以下情況下發生阻塞:試圖加鎖一個已經被鎖住的互斥量,等待某個條件變量,調用singwait等待尚未發生的信號,執行無法完成的I/O信號,由於內存頁錯誤。
  • 終止
    線程可以在啓動函數中返回來終止自己,或者調用 pthread_exit 退出,或者取消線程。

1. 線程ID

我們首先來看下線程的創建,線程的創建使用的是 pthread_create 函數:
C++
int pthread_create (pthread_t *thread,
pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);

功能:創建一個線程。
第一個參數 thread:新線程的id,如果成功則新線程的id回填充到tidp指向的內存
第二個參數 attr:線程屬性(調度策略,繼承性,分離性…)
第三個參數 start_routine:回調函數(新線程要執行的函數)
第四個參數 arg:回調函數的參數
返回值:成功返回0,失敗則返回錯誤碼

當線程被創建時,thread 被寫入一個標識符,我們使用這個標識符來引用被創建的線程。attr 可以用來詳細設定線程的屬性,一般爲 NULL,我們會在本篇博客的最後詳細介紹線程的屬性,start_routine 指向了一個函數地址,我們可以定義任意一個具有一個任意類型的參數並返回任意一個返回值的函數作爲線程的執行函數,最後一個 arg 傳入了該函數的參數。需要注意的是:

編譯時需要連接庫 libpthread
新線程可能在當前線程從函數 pthread_create 返回之前就已經運行了,甚至新線程可能在當前線程從函數 pthread_create 返回之前就已經運行完畢了。

對於一個線程,可以使用 pthread_self() 函數來獲取自己的線程號。
C++
int pthread_self (void);

功能:返回當前線程的線程 id。
返回值:調用線程的線程ID。

還有一個函數是 pthread_equal() 可以用來判斷兩個線程ID 是否一致。
C++
int pthread_equal (pthread_t tid1, pthread_t tid2);

功能:功能判斷兩個線程ID是否一致。
參數:tid1 tid2 需要判斷的ID
返回值:相等返回非0值,不相等返回0.

這樣,主線程可以將工作任務放在一個隊列中,並將每個任務添加一個進程ID標識,這樣相應的線程使用上面兩個函數就可以取出和自己相關的任務。

2. 初始線程/主線程

當c程序運行時,首先運行 main 函數。在線程代碼中,這個特殊的執行流被稱作初始線程或者主線程。你可以在初始線程中做任何普通線程可以做的事情。主線程的特殊性在於:

  • 它在 main 函數返回的時候,會導致進程結束,進程內所有的線程也將會結束。這可不是一個好的現象,你可以在主線程中調用 pthread_exit 函數,這樣進程就會等待所有線程結束時才終止。
  • 主線程接受參數的方式是通過 argcargv,而普通的線程只有一個參數 void*
  • 在絕大多數情況下,主線程在默認堆棧上運行,這個堆棧可以增長到足夠的長度。而普通線程的堆棧是受限制的,一旦溢出就會產生錯誤。

二、線程的運行

1. 線程的退出

退出線程使用的是 pthread_exit,需要注意的是:線程函數不能使用 exit() 退出,exit是危險的:如果進程中的任意一個線程調用了 exit_Exit_exit,那麼整個進程就會終止,普通的單個線程以以下三種方式退出,不會終止進程:
1. 從啓動函數中返回,返回值是線程的退出碼。
2. 線程可以被同一進程中的其他線程取消。
3. 線程調用 pthread_exit() 函數。

C
void pthread_exit(void *rval);

功能:退出一個線程。
參數:rval 是個無類型的指針,保存線程的退出碼,其他線程可以通過 pthread_join() 函數來接收這個值。

2. 線程的回收

類似於進程的 wait() 函數,線程也有相應的機制,稱爲線程的回收,和這個機制緊密相關的一個概念是線程的分離屬性

線程的分離屬性:

分離一個正在運行的線程並不影響它,僅僅是通知當前系統該線程結束時,其所屬的資源可以回收。
一個沒有被分離的線程在終止時會保留它的虛擬內存,包括他們的堆棧和其他系統資源,有時這種線程被稱爲殭屍線程
創建線程時默認是非分離的

如果線程具有分離屬性,線程終止時會被立刻回收,回收將釋放掉所有在線程終止時未釋放的系統資源和進程資源,包括保存線程返回值的內存空間、堆棧、保存寄存器的內存空間等。
終止被分離的線程會釋放所有的系統資源,但是你必須釋放由該線程佔有的程序資源。由 malloc() 或者 mmap() 分配的內存可以在任何時候由任何線程釋放,條件變量、互斥量、信號燈可以由任何線程銷燬,只要他們被解鎖了或者沒有線程等待。但是隻有互斥量的主人才能解鎖它,所以在線程終止前,你需要解鎖互斥量

將已創建的線程設置爲分離的有兩種方式:

  • 調用 pthread_detach() 這個函數,那麼當這個線程終止的時候,和它相關的系統資源將被自動釋放。
    C
    int pthread_detach(pthread_t thread);

    功能:分離一個線程,線程可以自己分離自己。
    參數:thread 指定線程的 id。
    返回值:成功返回0,失敗返回錯誤碼

  • 調用 pthread_join 會使指定的線程處於分離狀態(),如果指定線程已經處於分離狀態,那麼調用就會失敗。
    C
    int pthread_join(pthead_t tid, void **rval);

    功能:調用該函數的線程會一直阻塞,直到指定的線程 tid 調用 pthread_exit、從啓動函數返回或者被取消。
    參數:參數 tid 就是指定線程的 id,參數 rval 是指定線程的返回碼,如果線程被取消,那麼 rval 被置爲 PTHREAD_CANCELED
    返回值:該函數調用成功會返回0,失敗返回錯誤碼

3. 線程的取消

線程的取消類似於一個線程向另一個線程發送了一個信號,要求它終止,相應的取消函數很簡單:
C
int pthread_cancle(pthread_t tid);

功能:請求一個線程終止
參數:tid 指定的線程
返回值:成功返回0,失敗返回錯誤碼。

取消只是發送一個請求,並不意味着等待線程終止,而且發送成功也不意味着 tid 一定會終止,它通常需要被取消線程的配合。線程在很多時候會查看自己取消狀態,如果有就主動退出, 這些查看是否有取消的地方稱爲取消點

這時候,我們又有了兩個概念:取消狀態取消點

  • 取消狀態
    取消狀態,就是線程對取消信號的處理方式,忽略或者響應。線程創建時默認響應取消信號,相應的設置函數爲:
    C
    int pthread_setcancelstate(int state, int *oldstate);

    功能:設置本線程對取消請求(CANCEL 信號)的反應
    參數:state 有兩種值:PTHREAD_CANCEL_ENABLE(缺省,接收取消請求)和 PTHREAD_CANCEL_DISABLE(忽略取消請求);old_state 如果不爲NULL則存入原來的Cancel狀態以便恢復
    返回值:成功返回0,失敗返回錯誤碼。

    如果函數設置爲接收取消請求,還可以設置他的取消類型:
    取消類型,是線程對取消信號的響應方式,立即取消或者延時取消。線程創建時默認延時取消
    C
    int pthread_setcanceltype(int type, int *oldtype) ;

    功能:設置本線程取消動作的執行時機
    參數:type 由兩種取值:PTHREAD_CANCEL_DEFFERED 表示收到信號後繼續運行至下一個取消點再退出, PTHREAD_CANCEL_ASYCHRONOUS 標識立即執行取消動作(退出);兩者僅當 Cancel 狀態爲 Enable 時有效;oldtype 如果不爲NULL則存入運來的取消動作類型值。
    返回值:成功返回0,失敗則返回錯誤碼

  • 取消點
    取消點:取消一個線程,默認需要被取消線程的配合。線程在很多時候會查看自己是否有取消請求,如果有就主動退出, 這些查看是否有取消的地方稱爲取消點。很多地方都是包含取消點,包括 pthread_join()pthread_testcancel()pthread_cond_wait()pthread_cond_timedwait()sem_wait()sigwait() 以及 writeread 等大多數會阻塞的系統調用。

4. 線程的清除

線程可以安排它退出時的清理操作,這與進程的可以用atexit函數安排進程退出時需要調用的函數類似。這樣的函數稱爲線程清理處理程序。線程可以建立多個清理處理程序,處理程序記錄在棧中,所以這些處理程序執行的順序與他們註冊的順序相反
C
void pthread_cleanup_push(void (*rtn)(void*), void *args)//註冊處理程序
void pthread_cleanup_pop(int excute)//清除處理程序

當執行以下操作時調用清理函數,清理函數的參數由 args 傳入
1. 調用 pthread_exit
2. 響應取消請求
3. 用非零參數調用 pthread_cleanup_pop

return 不調用清理操作

五、線程的同步

有幾種方法可以很好的控制線程執行和訪問臨界區域,主要是互斥量和信號量。

1. 互斥量

爲什麼要使用互斥量?
當多個線程共享相同的內存時,需要每一個線程看到相同的視圖。當一個線程修改變量時,而其他線程也可以讀取或者修改這個變量,就需要對這些線程同步,確保他們不會訪問到無效的變量。
爲了讓線程訪問數據不產生衝突,這要就需要對變量加鎖,使得同一時刻只有一個線程可以訪問變量。互斥量本質就是鎖,訪問共享資源前對互斥量加鎖,訪問完成後解鎖。
當互斥量加鎖以後,其他所有需要訪問該互斥量的線程都將阻塞,當互斥量解鎖以後,所有因爲這個互斥量阻塞的線程都將變爲就緒態,第一個獲得cpu的線程會獲得互斥量,變爲運行態,而其他線程會繼續變爲阻塞,在這種方式下訪問互斥量每次只有一個線程能向前執行.

  1. 互斥量的初始化和銷燬

    互斥量用 pthread_mutex_t 類型的數據表示,在使用之前需要對互斥量初始化

    • 如果是動態分配的互斥量,可以調用 pthread_mutex_init()函數初始化
    • 如果是靜態分配的互斥量,還可以把它置爲常量 PTHREAD_MUTEX_INITIALIZER
    • 動態分配的互斥量在釋放內存之前需要調用 pthread_mutex_destroy()

    相應的處理函數爲
    C
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr); //動態初始化互斥量
    int pthread_mutex_destroy(pthread_mutex_t *mutex); //動態互斥量銷燬
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //靜態初始化互斥量

    mutex 爲需要創建或銷燬的互斥量;attr 爲新建互斥量的屬性,默認爲 PTHREAD_MUTEX_TIMED_NP, 即普通鎖。

  2. 加鎖和解鎖
    C
    int pthread_mutex_lock(pthread_mutex_t *mutex);

    成功返回0,失敗返回錯誤碼。如果互斥量已經被鎖住,那麼會導致該線程阻塞。

    C
    int pthread_mutex_trylock(pthread_mutex_t *mutex);

    成功返回0,失敗返回錯誤碼。如果互斥量已經被鎖住,不會導致線程阻塞。

    C
    int pthread_mutex_unlock(pthread_mutex_t *mutex);

    成功返回0,失敗返回錯誤碼。如果一個互斥量沒有被鎖住,那麼解鎖就會出錯。

  3. 死鎖
    死鎖:線程一直在等待鎖,而鎖卻無法解開。如果一個線程對已經佔有的互斥量繼續加鎖,那麼他就會陷入死鎖狀態。

    如何去避免死鎖?
    你可以小心的控制互斥量加鎖的順序來避免死鎖,例如所有的線程都在加鎖B之前先加鎖A,那麼這兩個互斥量就不會產生死鎖了。有的時候程序寫的多了互斥量就難以把控,你可以先釋放已經佔有的鎖,然後再加鎖其他互斥量。

互斥量使用要注意:

  1. 訪問共享資源時需要加鎖
  2. 互斥量使用完之後需要銷燬
  3. 加鎖之後一定要解鎖
  4. 互斥量加鎖的範圍要小
  5. 互斥量的數量應該少

2. 信號量

這裏說的信號量和前一篇說的信號量不同,這裏的信號量來自 POSIX 的實時擴展,而之前的信號量來自於 System V。兩者的函數接口相似,但是不用通用。這裏的相關信號量函數都以 sem_ 開頭。相關的函數有四個:

  1. sem_init 函數
    該函數用於創建信號量,其原型如下:
    C
    int sem_init(sem_t *sem, int pshared, unsigned int value);

    該函數初始化由 sem 指向的信號對象,設置它的共享選項,並給它一個初始的整數值。pshared 控制信號量的類型,如果其值爲0,就表示這個信號量是當前進程的局部信號量,否則信號量就可以在多個進程之間共享,valuesem 的初始值。
    調用成功時返回0,失敗返回-1.

  2. sem_wait 函數
    該函數用於以原子操作的方式將信號量的值減1。原子操作就是,如果兩個線程企圖同時給一個信號量加1或減1,它們之間不會互相干擾。它的原型如下:
    C
    int sem_wait(sem_t *sem);

    sem 指向的對象是由 sem_init 調用初始化的信號量。
    調用成功時返回0,失敗返回-1.

  3. sem_post 函數
    該函數用於以原子操作的方式將信號量的值加1。它的原型如下:
    C
    int sem_post(sem_t *sem);

    sem_wait 一樣,sem 指向的對象是由 sem_init 調用初始化的信號量。
    調用成功時返回0,失敗返回-1.

  4. sem_destroy 函數
    該函數用於對用完的信號量的清理。它的原型如下:
    C
    int sem_destroy(sem_t *sem);
    成功時返回0,失敗時返回-1.

3. 讀寫鎖

什麼是讀寫鎖,它與互斥量的區別:
讀寫鎖與互斥量類似,不過讀寫鎖有更高的並行性。互斥量要麼加鎖要麼不加鎖,而且同一時刻只允許一個線程對其加鎖。但是對於一個變量的讀取,完全可以讓多個線程同時進行操作。這時候讀寫鎖更爲實用。

  • 讀寫鎖有三種狀態,讀模式下加鎖,寫模式下加鎖,不加鎖。相應的使用方法爲:
    • 一次只有一個線程可以佔有寫模式下的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖。
    • 讀寫鎖在寫加鎖狀態時,在它被解鎖之前,所有試圖對這個鎖加鎖的線程都會阻塞。
    • 讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對其加鎖的線程都會獲得訪問權,但是如果線程希望以寫模式對其加鎖,它必須阻塞直到所有的線程釋放鎖。
    • 讀寫鎖在讀加鎖狀態時,如果有線程試圖以寫模式對其加鎖,那麼讀寫鎖會阻塞隨後的讀模式鎖請求。這樣可以避免讀鎖長期佔用,而寫鎖達不到請求。

讀寫鎖非常適合對數據結構讀次數大於寫次數的程序,當它以讀模式鎖住時,是以共享的方式鎖住的;當它以寫模式鎖住時,是以獨佔的模式鎖住的。

  1. 讀寫鎖的初始化和銷燬
    讀寫鎖在使用之前必須初始化,使用完需要銷燬。
    C
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    成功返回0 ,失敗返回錯誤碼

  2. 加鎖和解鎖
    讀模式加鎖
    C
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

    寫模式加鎖
    C
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

    解鎖
    C
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

    成功返回0

3. 條件變量

條件變量的引入:我們需要一種機制,當互斥量被鎖住以後發現當前線程還是無法完成自己的操作,那麼它應該釋放互斥量,讓其他線程工作。條件變量的作用就是可以採用輪詢的方式,不停的讓系統來幫你查詢條件。

  1. 條件變量的初始化和銷燬
    條件變量使用之前需要初始化,條件變量使用完成之後需要銷燬。
    C
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //靜態初始化條件變量
    int pthread_cond_init(pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr); //動態初始化條件變量
    int pthread_cond_destroy(pthread_cond_t *cond);//銷燬條件變量

    動態條件變量的初始化和銷燬函數返回值都是成功返回0,失敗返回錯誤代碼。attr 的值一般爲 NULL。詳細的設置在下個章節。

  2. 條件變量的等待和喚醒
    條件變量使用需要配合互斥量

    1. 使用 pthread_cond_wait 等待條件變爲真。傳遞給 pthread_cond_wait 的互斥量對條件進行保護,調用者把鎖住的互斥量傳遞給函數。
      這個函數將線程放到等待條件的線程列表上,然後對互斥量進行解鎖,這是個原子操作。當條件滿足時這個函數返回,返回以後繼續對互斥量加鎖。
      C
      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 abstime);

      這個函數與 pthread_cond_wait 類似,只是多一個 timeout,如果到了指定的時間條件還不滿足,那麼就返回。
      注意,這個時間是絕對時間。例如你要等待3分鐘,就要把當前時間加上3分鐘然後轉換到 timespec,而不是直接將3分鐘轉換到 timespec
    2. 當條件滿足的時候,需要喚醒等待條件的線程
      C
      int pthread_cond_broadcast(pthread_cond_t *cond); //喚醒等待條件的所有線程
      int pthread_cond_signal(pthread_cond_t *cond); //至少喚醒等待條件的某一個線程

      注意,一定要在條件改變以後在喚醒線程。
  3. 條件變量的使用
    條件變量主要使用在那些需要條件觸發的場景。譬如,一個經典的生產者消費者的問題。消費者等待生產者生產,如果單純的使用互斥量,當然也可以解決問題,但是在生產者沒有生產的時候,消費者就需要不停的輪詢,大大浪費了CPU資源。我們更期待的是等生產者生產後”通知”我們的消費者,所以我們使用條件變量:
    生產者線程的執行順序是:加鎖 -> 生產 -> pthread_cond_signal -> 釋放鎖
    消費者線程的執行順尋是:加鎖 -> while(沒有生產) pthread_cond_wait; 當沒有商品存在的時候,進入條件變量,此時條件變量首先釋放了鎖,然後阻塞等待 pthread_cond_signal 信號的發送,接收到信號之後,申請鎖,pthread_cond_wait結束, -> 執行相應的程序 -> 釋放鎖。

六、線程的控制

1. 線程屬性

線程的屬性用 pthread_attr_t 類型的結構表示,在創建線程的時候可以不用傳入NULL,而是傳入一個 pthread_attr_t 結構,由用戶自己來配置線程的屬性。pthread_attr_t 結構中定義的線程屬性有很多:

名稱 描述
detachstate 線程的分離狀態
guardsize 線程棧末尾的警戒區域大小(字節數)
stacksize 線程棧的最低地址
stacksize 線程棧的大小(字節數)

1. 線程屬性的初始化和銷燬
線程的初始化和銷燬使用下面兩個函數:
C
int pthread_attr_init(pthread_attr_t *attr); //線程屬性初始化
int pthread_attr_destroy(pthread_attr_t *attr); //線程屬性銷燬

如果在調用 pthread_attr_init 初始化屬性的時候分配了內存空間,那麼 pthread_attr_destroy 將釋放內存空間。除此之外,pthread_atty_destroy 還會用無效的值初始化 pthread_attr_t 對象,因此如果該屬性對象被誤用,會導致創建線程失敗。pthread_attr_t 類型對應用程序是不透明的,也就是說應用程序不需要了解有關屬性對象內部結構的任何細節,因而可以增加程序的可移植性。

  1. 線程的分離屬性
    線程的分離屬性已經在前面的章節介紹過了,如果在創建線程的時候就知道不需要了解線程的終止狀態,那麼可以修改 pthread_attr_t 結構體的 detachstate 屬性,讓線程以分離狀態啓動。線程的分離屬性有兩種合法值:
    PTHREAD_CREATE_DETACHED 分離的;
    PTHREAD_CREATE_JOINABLE 非分離的,可連接的;
    設置線程分離屬性的步驟

    1. 定義線程屬性變量 pthread_attr_t attr
    2. 初始化 attrpthread_attr_init(&attr)
    3. 設置線程爲分離或非分離 pthread_attr_setdetachstate(&attr, detachstate)
    4. 創建線程 pthread_create(&tid, &attr, thread_fun, NULL)
  2. 線程的棧屬性
    對一個進程,他的虛擬空間的大小是固定的,如果程序啓動了大量的線程,因爲所有的線程共享進程的的虛擬地址空間,所以按照默認的棧大小,虛擬空間就會不足,需要調小棧空間。或者某個線程使用了大量的自動變量,這時候需要調大棧空間,具體的分配使用不再詳說,介紹下相關的函數:
    C
    int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); //修改棧屬性
    int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); //獲取棧屬性
    int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); //單獨設置棧屬性
    int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); //單獨獲取棧屬性
    int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize); //單獨設置棧屬性
    int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize); //單獨獲取棧屬性

    上述函數的返回值都是成功返回0,失敗返回相應的錯誤編碼。
    對於遵循POSIX標準的系統來說,不一定要支持線程的棧屬性,因此你需要檢查

    1. 在編譯階段使用
      _POSIX_THREAD_ATTR_STACKADDR_POSIX_THREAD_ATTR_STACKSIZE 符號來檢查系統是否支持線程棧屬性。
    2. 在運行階段把
      _SC_THREAD_ATTR_STACKADD_SC_THREAD_THREAD_ATTR_STACKSIZE 傳遞給 sysconf 函數檢查系統對線程棧屬性的支持。
  3. 線程的其他屬性:
    線程還有一些屬性沒有在 pthread_attr_t 結構體中定義,如已經在上面介紹過的線程的取消狀態和線程的取消類型,還有沒有介紹的線程的併發度,這些就不再詳細描述。

2. 同步屬性

  1. 互斥量的屬性
    就像線程有屬性一樣,線程的同步互斥量也有屬性,比較重要的是進程共享屬性和類型屬性。互斥量的屬性用 pthread_mutexattr_t 類型的數據表示,當然在使用之前必須進行初始化,使用完成之後需要進行銷燬:
    C
    int pthread_mutexattr_init(pthread_mutexattr_t *attr); //互斥量屬性初始化
    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); //互斥量屬性銷燬
    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,
    int *restrict pshared); //獲取互斥量屬性
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
    int pshared); //設置互斥量屬性

    上述函數的返回值都是成功返回0,失敗返回相應的錯誤編碼。
    互斥量屬性的相關的值有:

    • PTHREAD_MUTEX_TIMED_NP,缺省值,當一個線程加鎖以後,其餘請求鎖的線程將形成一個等待隊列,並在解鎖後按優先級獲得鎖。
    • PTHREAD_MUTEX_RECURSIVE_NP,嵌套鎖,允許同一個線程對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同線程請求,則在加鎖線程解鎖時重新競爭。
    • PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個線程請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP類型動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
    • PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖類型,僅等待解鎖後重新競爭。
  2. 讀寫鎖的屬性
    讀寫鎖也有屬性,它只有一個進程共享屬性:
    C
    int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
    int pthread_rwlockattr_init(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);

    上述函數的返回值都是成功返回0,失敗返回相應的錯誤編碼。

  3. 條件變量的屬性
    條件變量也有進程共享屬性:
    C
    int pthread_condattr_destroy(pthread_condattr_t *attr);
    int pthread_condattr_init(pthread_condattr_t *attr);
    int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
    int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);

    上述函數的返回值都是成功返回0,失敗返回相應的錯誤編碼。

3. 私有數據

應用程序設計中有必要提供一種變量,使得多個函數多個線程都可以訪問這個變量(看起來是個全局變量),但是線程對這個變量的訪問都不會彼此產生影響(貌似不是全局變量哦),但是你需要這樣的數據,比如 errno。那麼這種數據就是線程的私有數據,儘管名字相同,但是每個線程訪問的都是數據的副本。

在使用私有數據之前,你首先要創建一個與私有數據相關的鍵,要來獲取對私有數據的訪問權限 。這個鍵的類型是 pthread_key_t:
C
int pthread_key_create(pthread_key_t *key, void (*destructor)(voi8d*));

創建的鍵放在key指向的內存單元,destructor 是與鍵相關的析構函數。當線程調用 pthread_exit 或者使用 return 返回,析構函數就會被調用。當析構函數調用的時候,它只有一個參數,這個參數是與key關聯的那個數據的地址,因此你可以在析構函數中將這個數據銷燬。

鍵使用完之後也可以刪除,當鍵刪除之後,與它關聯的數據並沒有銷燬:
C
int pthread_key_delete(pthread_key_t key);

有了鍵之後,你就可以將私有數據和鍵關聯起來,這樣就就可以通過鍵來找到數據。所有的線程都可以訪問這個鍵,但他們可以爲鍵關聯不同的數據。
C
int pthread_setspecific(pthread_key_t key, const void *value);

將私有數據與key關聯

C
void *pthread_getspecific(pthread_key_t key);

獲取私有數據的地址,如果沒有數據與key關聯,那麼返回空。

有些事需要且只能執行一次(比如互斥量初始化)。通常當初始化應用程序時,可以比較容易地將其放在main函數中。但當你寫一個庫函數時,就不能在main裏面初始化了,你可以用靜態初始化,但使用一次初始(pthread_once_t)會比較容易些。

  • 首先要定義一個 pthread_once_t變量,這個變量要用宏 PTHREAD_ONCE_INIT 初始化。然後創建一個與控制變量相關的初始化函數
    C
    pthread_once_t once_control = PTHREAD_ONCE_INIT;
    void init_routine()
    {
    //初始化互斥量
    //初始化讀寫鎖
    ......
    }

  • 接下來就可以在任何時刻調用pthread_once函數
    C
    int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));

    功能:本函數使用初值爲 PTHREAD_ONCE_INITonce_control 變量保證 init_routine() 函數在本進程執行序列中僅執行一次。在多線程編程環境下,儘管pthread_once() 調用會出現在多個線程中,init_routine() 函數僅執行一次,究竟在哪個線程中執行是不定的,是由內核調度來決定。”一次性函數”的執行狀態有三種:NEVER(0). IN_PROGRESS(1). DONE (2),用 once_control 來表示 pthread_once() 的執行狀態:

    1. 如果 once_control 初值爲0,那麼 pthread_once 從未執行過,init_routine() 函數會執行。
    2. 如果 once_control 初值設爲1,則由於所有 pthread_once() 都必須等待其中一個激發”已執行一次”信號, 因此所有 pthread_once () 都會陷入永久的等待中,init_routine() 就無法執行
    3. 如果 once_control 設爲2,則表示 pthread_once() 函數已執行過一次,從而所有 pthread_once()都會立即返回,init_routine()就沒有機會執行。當pthread_once函數成功返回,once_control就會被設置爲2

4. 線程的信號

在線程中使用信號,與在進程中使用信號機制有着根本的區別。在進程環境中,對信號的處理是異步的(我們完全不知到信號會在進程的那個執行點到來!)。但是在多線程中處理信號的原則完全不同,它的基本原則是:將對信號的異步處理,轉換成同步處理,也就是說用一個線程專門的來“同步等待”信號的到來,而其它的線程可以完全不被該信號中斷/打斷(interrupt)。

  • 信號的發送
    線程中信號的發送並不使用 kill() 函數,而是有專門的進程信號函數:
    C
    int pthread_kill(pthread_t thread, int sig);

    功能:向指定ID的線程發送信號。
    參數:thread 進程標識符,sig 發送的信號。

    如果線程代碼內不做處理,則按照信號默認的行爲影響整個進程,也就是說,如果你給一個線程發送了 SIGQUIT ,但線程卻沒有實現 signal 處理函數,則整個進程退出。如果要獲得正確的行爲,就需要在線程內實現 signal(SIGKILL,sig_handler) 了。所以,如果 sig 不是0,那一定要清楚到底要幹什麼,而且一定要實現線程的信號處理函數,否則,就會影響整個進程。如果 sig 是0,這是一個保留信號,其實並沒有發送信號,作用是用來判斷線程是不是還活着。

  • 信號的接收
    在多線程代碼中,總是使用 sigwait 或者 sigwaitinfo 或者 sigtimedwait 等函數來處理信號。而不是 signal 或者 sigaction 等函數。因爲在一個線程中調用 signal 或者 sigaction 等函數會改變所以線程中的信號處理函數。而不是僅僅改變調用 signal/sigaction 的那個線程的信號處理函數。

5. 線程與fork

當線程調用 fork() 函數時,就爲子進程創建了整個進程地址空間的副本,子進程通過繼承整個地址空間的副本,也會將父進程的互斥量、讀寫鎖、條件變量的狀態繼承過來。也就是說,如果父進程調用 fork() 的線程中佔有鎖,那麼在子進程中也佔有鎖,這是非常不安全的,因爲不是子進程自己鎖住的,它無法解鎖。

子進程內部只有一個線程,由父進程中調用 fork() 函數的線程副本構成。如果調用 fork() 的線程將互斥量鎖住,那麼子進程會拷貝一個 pthread_mutex_lock 副本,這樣子進程就有機會去解鎖了。或者互斥量根本就沒被加鎖,這樣也是可以的,但是你不能確保永遠是這樣的情況。

pthread_atfork 函數給你創造了這樣的條件,它會註冊三個函數
C
int pthread_atfork(void (*prepare)(void);
void (*parent)(void);
void (*child)(void));

prepare 是在 fork() 調用之前會被調用的,他的任務是獲取所有父進程中定義的鎖。
parentfork() 創建了子進程但返回之前在父進程中調用,他的任務是對所有 prepare 中獲取的鎖解鎖。
childfork() 創建了子進程但返回之前在子進程中調用,他的任務是對所有 prepare 中獲取的鎖解鎖。

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