1. 程式人生 > >Linux核心中的執行緒及多執行緒

Linux核心中的執行緒及多執行緒

一、執行緒的概念、理解及特點

    1.執行緒的概念:

        至今為止,Linux下還是沒有“真正的執行緒”。談到執行緒就不得不提到程序這概念,程序是系統中程式執行和資源分配的基本單位。每個程序都擁有自己的資料段,程式碼段和堆疊段,這就造成了程序在進行切換等操作時都需要有比較負責的上下文切換等動作.為了進一步減少處理機的空轉時間支援多個處理器和減少上下文切換開銷,程序在演化中出現了另一個概念--執行緒。它是一個程序內的基本排程單位,也可以稱為輕量級程序。執行緒是在供享記憶體空間中併發的多道執行路徑,它們共享一個程序的資源,如檔案描述和訊號處理。因此,大大減少了上下文切換的開銷。

    2.執行緒的理解:

       (1).執行緒有的只是一組共享記憶體/PID/執行環境的程序,這被稱為1:1的執行緒模型,因為每個“執行緒”都要有一個核心排程單元與之對應(因為在OS核心中,程序這個概念並不是很清晰,所以權且稱之為排程單元吧)。這樣做最大的好處(也是linux本人堅持這樣做的原因)就是簡單,但實際上“執行緒”基本變成了“程序”,要知道程序上下文切換的代價比較高(這也是多執行緒模型被引入的原因之一),所以在新城數量很多時,系統的執行緒表(實際上是程序表)會很大,切換的效率會急劇下降,造成執行效率較低。

       (2).另外還有兩種執行緒模型:

            a.一種是m:1模型,也就是多個使用者執行緒對應一個核心排程單元,換言之就是使用者完全自己做排程,核心不為此提供任何支援,在早期Unix系統上,因為系統本身不支援多執行緒,多已出現了很多這樣的庫,這樣做系統開銷比較小,因為使用者執行緒的切換完全不涉及程序上下文的切換,但是這樣做最大的問題就是“執行緒”切換問題很多。

            b.另一種是m:n模型,這種模型比較複雜,簡單說就是核心準備n個排程單元,由這些排程單元輪換執行m個使用者“執行緒”,這樣做最大的好處就是排程由核心處理,而且由於切換的是“執行緒”,而不是“程序”,所以切換上下文時的開銷較小,另外n的個數可以由系統決定,以充分利用硬體的多處理能力。

     3.執行緒的特點:

        (1).使用者級執行緒:主要解決的是上下文切換的問題,它的排程演算法和排程過程會全部由使用者自行選擇解決,在執行時不需要特定的核心支援。作業系統往往會提供一個使用者空間的執行緒庫,該使用者程式庫提供了執行緒的建立、排程和撤銷等功能,而核心仍然對程序進行管理。如果一個程序中的某一個執行緒呼叫了一個阻塞的系統呼叫函式,那麼該程序包括該程序中的執行緒也同時被阻塞。這種使用者級執行緒的主要缺點是在一個程序中的多個執行緒的排程中無法發揮多處理器的優勢。

       (2).輕量級的程序:輕量程序是核心支援的使用者執行緒,是核心執行緒的一種抽象物件。每個執行緒擁有一個或多個輕量級執行緒,而每個輕量級執行緒分別被繫結在一個核心線上。

       (3).核心執行緒:這個執行緒不允許不同程序中的執行緒按照同一相對優先排程方法進行排程,這樣就可以發揮多處理器併發優勢。

      注意:a.程序是系統中程式執行和資源分配的基本單位。
                 b.每個程序都擁有自己的資料段、程式碼段和堆疊段。

二、程序與執行緒的區別

     1.定義方面:程序是程式在某個資料集合上的一次執行活動;執行緒是程序中的一個執行路徑。

     2.角色方面:在支援執行緒機制的系統中,程序是系統資源分配的單位,執行緒是系統排程的單位。

     3.資源共享方面:程序之間不能共享資源,而執行緒共享所在程序的地址空間和其它資源。同時執行緒還有自己的棧和棧指標,程式計數器等暫存器。

     4.獨立性方面:程序有自己獨立的地址空間,而執行緒沒有,執行緒必須依賴於程序而存。

三、執行緒的建立、等待及終止

     程序在各自在自己的地址空間中執行,程序間通訊要通過程序間通訊機制實現,但是一個程序的地址空間中可以執行多個執行緒,這些執行緒除了共享資料段還共享檔案描述符表,使用者id組id,和當前工作目錄,errno變數。但同一程序中的執行緒還有其所獨有的:執行緒id、上下文(暫存器、程式計數器、棧指標)、排程優先順序、等等。

    1.執行緒的建立函式:

  1. int pthread_create(pthread_t*thread,const pthread_att_t*attr,void*(*start_routine)(void*),void*arg);  
         若成功返回0,錯誤返回錯誤號,當一個執行緒呼叫此函式繼續往下執行,新的執行緒所執行的程式碼由函式指標start_routine決定,start_routine接受一個通過pthread_creat函式傳進來的void*型別引數argstart_routine返回時此執行緒結束。別的執行緒呼叫pthread_join得到start_routine的返回值。

     2.執行緒的等待:

  1. int pthread_join(pthread_t thread,void **retval)  

        呼叫該函式的執行緒將掛起等待,直到idthread的執行緒終止。

     3.執行緒的終止:

       (1).從執行緒函式返回(main函式除外)

       (2).一個執行緒呼叫pthread_cancel終止同一程序中的另一執行緒

       (3).執行緒呼叫pthread_exit(void *retval)終止自己


檢視執行緒不同終止方式返回,value_ptr所指向的記憶體的值:




執行結果:

四、執行緒的分離、同步與互斥

      1. 執行緒的分離:

         執行緒終止後,執行緒狀態一直保留到其他執行緒呼叫pthread_join獲取狀態為止,但執行緒也可以被置detach狀態,這樣一旦終止就立刻回收它所佔有的資源,而不保留終止狀態。

      2.分離函式:

int pthread_detach(pthread_t thread);

        可結合的執行緒能被其它執行緒收回其資源和殺死,被其他執行緒回收之前,儲存器資源不釋放,一個分離的執行緒是不能被其他執行緒回收或殺死的,它的儲存器資源在終止時由系統自動釋放。

        呼叫pthread_join後,如果執行緒沒有執行結束,呼叫者會被阻塞,但有時當主執行緒建立多個子執行緒進行處理,並不像希望呼叫pthread_join阻塞,這時就可以置執行緒為分離狀態,這樣一來執行緒執行結束後會自動釋放資源。

      3.執行緒的同步:

多執行緒的程式,訪問衝突很普遍,可以引入互斥鎖(Mutex),獲得鎖的執行緒可以進行讀寫修改操作,然後釋放鎖給其它執行緒。

A:實現互斥鎖的操作:

lock:

      movb $0,%al

     xchgb %al,mutex

if(al暫存器的內容>0){

      return 0;

}else 

     掛起等待;

goto lock;

unlock:

    movb $1 ,mutex

    喚醒等待Mutex的執行緒;

return 0;

其中“掛起等待”和“喚醒等待執行緒”的實現:每個Mutex有一個等待佇列,一個執行緒要在Mutex上掛起等待,首先要將自己加入到等待佇列中,然後置執行緒為睡眠狀態,然後呼叫排程器函式切換到別的執行緒,一個執行緒要喚醒等待佇列中的其他執行緒,只需從等待佇列中取出一項,將睡眠狀態改為就緒,加入就緒佇列。

引起死鎖的兩種典型情況:

如果所有執行緒在需要多個鎖時都按相同的先後順序(常見的是按Mutex變數的地址順序)獲得鎖,則不會出現死鎖。也可以使用pthread_mutex_trylock呼叫替代pthread_mutex_lock

 B:條件變數

執行緒間的同步還有這樣這種情況:執行緒A需要等某個條件成立才能繼續往下執行,現在這個條
件不成立,執行緒A就阻塞等待,而執行緒B在執行過程中使這個條件成立了,就喚醒執行緒A繼續執

C:Semaphore(訊號量)

訊號量和Mutex類似,表示可用資源的數量,訊號量的數量大於1。

int sem_init(sem_t *sem,int pshared,unsigned int value);value:可用資源數量;pshared=0,表示同一程序的執行緒同步

int sem_wait(sem_t *sem);semaphore減1,如果呼叫時已經為0,則掛起等待

int sem_trywait(sem_t *sem);

int sem_post(sem_t *sem);釋放資源,semaphore加1

int sem_destory(sem_t *sem);

D:讀寫鎖

一個讀寫鎖只能有一個寫者或多個讀者,但不能即有讀者又有寫者。

五,、執行緒的分離與結合屬性

    執行緒屬性識別符號:

pthread_attr_t 包含在 pthread.h 標頭檔案中。

  1. typedefstruct
  2. {    
  3.     int                   etachstate;      //執行緒的分離狀態  
  4.     int                   schedpolicy;     //執行緒排程策略  
  5.     structsched_param     schedparam;      //執行緒的排程引數  
  6.     int                   inheritsched;    //執行緒的繼承性  
  7.     int                   scope;           //執行緒的作用域  
  8.     size_t                guardsize;       //執行緒棧末尾的警戒緩衝區大小  
  9.     int                   stackaddr_set;   //執行緒的棧設定  
  10.     void*                 stackaddr;       //執行緒棧的位置  
  11.     size_t                stacksize;       //執行緒棧的大小  
  12. }pthread_attr_t;   

       在任何一個時間點上,執行緒是可結合的(joinable),或者是分離的(detached)。一個可結合的執行緒能夠被其他執行緒收回其資源和殺死;在被其他執行緒回收之前,它的儲存器資源(如棧)是不釋放的。相反,一個分離的執行緒是不能被其他執行緒回收或殺死的,它的儲存器資源在它終止時由系統自動釋放。 為了避免儲存器洩漏,每個可結合線程都應該要 麼被顯示地回收,即呼叫pthread_join;要麼通過呼叫pthread_detach函式被分離。 由於呼叫pthread_join後,如果該執行緒沒有執行結束,呼叫者會被阻塞。因此,我們就可以在子執行緒中加入pthread_detach(pthread_self())或者父執行緒呼叫pthread_detach(thread_id)(非阻塞,可立即返回) 。 這樣將子執行緒的狀態設定為分離的(detached),如此一來,該執行緒執行結束後會自動釋放所有資源。