1. 程式人生 > >Linux多執行緒程式設計---執行緒間同步(互斥鎖、條件變數、訊號量和讀寫鎖)

Linux多執行緒程式設計---執行緒間同步(互斥鎖、條件變數、訊號量和讀寫鎖)

本篇博文轉自http://zhangxiaoya.github.io/2015/05/15/multi-thread-of-c-program-language-on-linux/

Linux下提供了多種方式來處理執行緒同步,最常用的是互斥鎖、條件變數、訊號量和讀寫鎖。 
下面是思維導圖:

簡介

程序— 資源分配的最小單位
執行緒— 程式執行的最小單位

程序是一個程式的一個例項,擁有自己獨立的各種段(資料段,程式碼段等等),每次建立一個程序需要從作業系統分配這些資源給他,消耗一定的時間,在linux下C語言建立一個程序使用fork()函式;
執行緒是一個輕量級的程序,除了自己少數的資源,不用用其他資源,且一個程序可以建立多個執行緒,這些執行緒共享程序的資源,建立執行緒的時間要比建立程序少很多,(幾十分之一),從函式角度是使用pthread_create()

建立。
使用執行緒處理檔案I/O或者socket處理都是非常有優勢的,將一個大人物分解成若干個小任務,每個執行緒處理一個任務,執行緒之間切換不需要花很多時間,而且執行緒之間資料交換很方便,共享儲存區。

C語言中使用多執行緒的函式

表 1. 執行緒函式列表

物件 操作 Linux Pthread API Windows SDK 庫對應 API
執行緒 建立 pthread_create CreateThread
退出 pthread_exit ThreadExit
等待 pthread_join WaitForSingleObject
互斥鎖 建立 pthread_mutex_init CreateMutex
銷燬 pthread_mutex_destroy CloseHandle
加鎖 pthread_mutex_lock WaitForSingleObject
解鎖 pthread_mutex_unlock ReleaseMutex
條件 建立 pthread_cond_init CreateEvent
銷燬 pthread_cond_destroy CloseHandle
觸發 pthread_cond_signal SetEvent
廣播 pthread_cond_broadcast SetEvent / ResetEvent
等待 pthread_cond_wait / pthread_cond_timedwait SingleObjectAndWait

多執行緒開發在 Linux 平臺上已經有成熟的 Pthread 庫支援。其涉及的多執行緒開發的最基本概念主要包含四點:執行緒,互斥鎖,條件變數、讀寫鎖。其中,執行緒操作又分執行緒的建立,退出,等待 3 種。互斥鎖則包括 4 種操作,分別是建立,銷燬,加鎖和解鎖。條件操作有 5 種操作:建立,銷燬,觸發,廣播和等待。其他的一些執行緒擴充套件概念,如訊號燈等,都可以通過上面的三個基本元素的基本操作封裝出來。

建立執行緒

int pthread_create(pthread_t * tid, const pthread_attr_t * attr, void * ( * func) (void * ), void * arg);
其返回值是一個整數,若建立程序成功返回0,否則,返回其他錯誤程式碼,也是正整數。

建立執行緒需要的引數:

  • 執行緒變數名:pthread_t *型別,是標示執行緒的id,一般是無符號整形,這裡也可以是引用型別,目的是用於返回建立執行緒的ID
  • 執行緒的屬性指標:制定執行緒的屬性,比如執行緒優先*級初始棧大小等,通常情況使用的都是指標。
  • 建立執行緒的程式程式碼:一般是函式指標,程序建立後執行該函式指標只想的函式。
  • 程式程式碼的引數:若執行緒執行的函式包含由若干個引數,需要將這些引數封裝成結構體,並傳遞給它指標。
    建立執行緒的函式的形式如下:

結束執行緒

結束程序的函式定義如下:

void pthread_exit (void *status);
引數是指標型別,用於儲存執行緒結束後返回狀態。

執行緒等待

int pthread_join (pthread_t tid, void ** status);

  • 第一個引數表示要等待的程序的id;
  • 第二引數表示要等待的程序的返回狀態,是個二級指標。

執行緒建立後怎麼執行,新執行緒和老執行緒誰先執行這些不是程式來決定,而是由作業系統進行排程的,但是在程式設計的時候我們常常需要多個執行緒配合工作,比如在結束某個執行緒之前,需要等待另外一個執行緒的處理結果(返回狀態等資訊),這時候就需要使用執行緒等待函式,這個函式的定義如下:

其他關於程序的函式

  1. 返回當前執行緒ID

    pthread_t pthread_self (void);
    用於返回當前程序的ID

  2. 制定執行緒變成分裂狀態

    int pthread_detach (pthread_t tid);
    引數是指定執行緒的ID,指定的ID的執行緒變成分離狀態;若指定執行緒是分離狀態,則 如果執行緒退出,那麼它所有的資源都將釋放,如果執行緒不是分離狀態,執行緒必須保留它的執行緒ID、退出狀態,直到其他執行緒對他呼叫的pthread_join()函式

參考例項一

程式碼如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

void print_message_func(void *ptr);

int main()
{
    int tmp1,tmp2;
    void *retival;
    pthread_t thread1,thread2;
    char *message1 = "thread1";
    char *message2 = "thread2";

    int ret_thread1,ret_thread2;
    ret_thread1 = pthread_create(&thread1,NULL,(void *)&print_message_func,(void *)message1);

    if(ret_thread1 == 0)
        printf("create thread 1 true\n");
    else
        printf("create thread 1 false\n");

    tmp1 = pthread_join(thread1,&retival);
    printf("thread 1 return value (tmp1) is %d\n",tmp1);

    if(tmp1 != 0)
        printf("cannot join with thread 1\n");

    ret_thread2 = pthread_create(&thread2,NULL,(void *)&print_message_func,(void *)message2);

    if(ret_thread2 == 0)
        printf("create thread 2 true\n");
    else
        printf("create thread 2 false\n");

    tmp2 = pthread_join(thread2,&retival);
    printf("thread 2 return value (tmp2) is %d\n",tmp2);
    if(tmp2 != 0)
        printf("cannot join with thread 2\n");
}

void print_message_func(void *ptr)
{
    for(int i=0;i<5;++i)
    {
        printf("%s:%d\n",(char*)ptr,i);
    }
}

這個程式碼比較簡單,就是演示這幾個常用函式的使用。

這裡是純C語言程式,在Linux下的編譯命令是gcc test.c -o test -lpthread,執行程式是./test,後面的程式同樣

多執行緒的同步與互斥

鎖機制

多執行緒之間可能需要互斥的訪問一些全域性變數,這就需要互斥的來訪問,這些需要共享訪問的欄位被稱作是臨界資源,訪問臨界資源的程式段稱作是臨界區
實現執行緒間的互斥與同步機制的是鎖機制,下面是常用的鎖機制的函式和類。

  1. pthread_mutex_t mutex 鎖物件
  2. pthread_mutex_init(&mutex,NULL) 在主執行緒中初始化鎖為解鎖狀態
  3. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 編譯時初始化鎖位解鎖狀態
  4. pthread_mutex_lock(&mutex)(阻塞加鎖)訪問臨界區加鎖操作
  5.  pthread_mutex_trylock( &mutex)(非阻塞加鎖); pthread_mutex_lock() 類似,不同的是在鎖已經被佔據時返回 EBUSY 而不是掛起等待。
  6. pthread_mutex_unlock(&mutex): 訪問臨界區解鎖操作

參考例項二(不加鎖訪問互斥全域性變數)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int sharei = 0;
void increase_num(void);

int main()
{
  int ret;
  pthread_t thread1,thread2,thread3;
  ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);

  pthread_join(thread1,NULL);
  pthread_join(thread2,NULL);
  pthread_join(thread3,NULL);

  printf("sharei = %d\n",sharei);

  return 0;
}

void increase_num(void)
{
  long i,tmp;
  for(i =0;i<=10000;++i)
  {
    tmp = sharei;
    tmp = tmp + 1;
    sharei = tmp;
  }
}

編譯執行結果,多執行幾次,發現結果都不一樣。這就是因為對於全域性變數,沒有新增互斥鎖,導致的問題。

參考例項三 (訪問全域性變數新增互斥鎖)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int sharei = 0;
void increase_num(void);
// add mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int main()
{
  int ret;
  pthread_t thread1,thread2,thread3;
  ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);

  pthread_join(thread1,NULL);
  pthread_join(thread2,NULL);
  pthread_join(thread3,NULL);

  printf("sharei = %d\n",sharei);

  return 0;
}

void increase_num(void)
{
  long i,tmp;
  for(i =0;i<=10000;++i)
  {
    // lock
    if(pthread_mutex_lock(&mutex) != 0)
    {
      perror("pthread_mutex_lock");
      exit(EXIT_FAILURE);
    }
    tmp = sharei;
    tmp = tmp + 1;
    sharei = tmp;
    // unlock
    if(pthread_mutex_unlock(&mutex) != 0)
    {
      perror("pthread_mutex_unlock");
      exit(EXIT_FAILURE);
    }
  }
}

新增互斥鎖後,就發現,多次執行的結果都是一樣的。

  1. 其實這裡的加鎖不是對共享變數(全域性變數)或者共享記憶體進行保護,這裡的加鎖實際上是對臨界區的控制,所謂的臨界區就是訪問臨界資源的那一段程式碼,這段程式碼對臨界資源進行多種操作,正確的情況是不允許這段程式碼執行到一半,處理器使用權就被其他執行緒搶走,所以這段程式碼具有原子性,即要麼執行,要麼不執行,不能執行到一半就被搶走處理權,這樣就會造成共享資料被汙染。
  2. 還有一點,新增鎖來控制臨界區是有代價的,這個代價表現出來就是時間的額外開銷,內部過程是因為要保護現場,會利用一些資源,也需要處理器處理的時間。

訊號量機制

鎖機制使用是有限制的,鎖只有兩種狀態,即加鎖和解鎖,對於互斥的訪問一個全域性變數,這樣的方式還可以對付,但是要是對於其他的臨界資源,比如說多臺印表機等,這種方式顯然不行了。
訊號量機制在作業系統裡面學習的比較熟悉了,訊號量是一個整數計數器,其數值表示空閒臨界資源的數量。
當有程序釋放資源時,訊號量增加,表示可用資源數增加;當有程序申請到資源時,訊號量減少,表示可用資源數減少。這個時候可以把鎖機制認為是0-1訊號量。
關於訊號量機制的函式。

int sem_init(sem_t * sem, int pshared, unsigned int value);初始化訊號量

- 成功返回0,失敗返回-1;
- 引數sem:表示指向訊號結構的指標。
- 引數pshared:不是0 的時候該訊號量在程序間共享,否則只能在當前程序的所有執行緒間共享。
- 引數value:訊號量的初始值。

int sem_wait(sem_t *sem); 訊號量減一操作,有執行緒申請資源

- 成功返回0,否則返回-1
- 引數sem:指向一個訊號量的指標

int sem_post(sem_t *sem);訊號量加一操作,有執行緒釋放資源

- 成功返回0,否則返回-1
- 引數sem:指向一個訊號量指標

int sem_destroy(sem_t *sem); 銷燬訊號量。

- 成功返回0,否則返回-1
- 引數sem:指向一個訊號量的指標。

參考例項四(生產者消費者)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

#define MAXSIZE 10

int stack[MAXSIZE];

int size =0;
sem_t sem;

void privide_data(void)
{
  int i;
  for(i =0;i<MAXSIZE;++i)
  {
    stack[i] = i;
    sem_post(&sem);
  }
}

void handle_data(void)
{
  int i;
  while((i = size ++) <MAXSIZE)
  {
    sem_wait(&sem);
    printf("cross : %d X %d = %d \n",stack[i],stack[i],stack[i] * stack[i]);
    sleep(1);
  }
}

int main()
{
  pthread_t privider,handler;
  sem_init(&sem,0,0);
  pthread_create(&privider,NULL,(void *)&privide_data,NULL);
  pthread_create(&handler,NULL,(void *)&handle_data,NULL);
  pthread_join(privider,NULL);
  pthread_join(handler,NULL);
  sem_destroy(&sem);

  return 0;
}

這段程式碼是經典的生產者消費者問題,只有當生產者把資源放入儲存區,消費者才能取得。

 

 

附:(知識點)

Linux 執行緒程式設計中的 5 條經驗

儘量設定 recursive 屬性以初始化 Linux 的互斥變數

互斥鎖是多執行緒程式設計中基本的概念,在開發中被廣泛使用。其呼叫次序層次清晰簡單:建鎖,加鎖,解鎖,銷燬鎖。但是需要注意的是,與諸如 Windows 平臺的互斥變數不同,在預設情況下,Linux 下的同一執行緒無法對同一互斥鎖進行遞迴加速,否則將發生死鎖。

所謂遞迴加鎖,就是在同一執行緒中試圖對互斥鎖進行兩次或兩次以上的行為。其場景在 Linux 平臺上的程式碼可由清單 1 所示。

清單 1. Linux 重複對互斥鎖加鎖例項

1

2

3

4

5

6

7

8

9

10

11

12

// 通過預設條件建鎖

    pthread_mutex_t *theMutex = new pthread_mutex_t;

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutex_init(theMutex,&attr);

    pthread_mutexattr_destroy(&attr);

 

    // 遞迴加鎖

    pthread_mutex_lock (theMutex);

    pthread_mutex_lock (theMutex);

    pthread_mutex_unlock (theMutex);

    pthread_mutex_unlock (theMutex);

在以上程式碼場景中,問題將出現在第二次加鎖操作。由於在預設情況下,Linux 不允許同一執行緒遞迴加鎖,因此在第二次加鎖操作時執行緒將出現死鎖。

Linux 互斥變數這種奇怪的行為或許對於特定的某些場景會所有用處,但是對於大多數情況下看起來更像是程式的一個 bug 。畢竟,在同一執行緒中對同一互斥鎖進行遞迴加鎖在尤其是二次開發中經常會需要。

這個問題與互斥鎖的中的預設 recursive 屬性有關。解決問題的方法就是顯式地在互斥變數初始化時將設定起 recursive 屬性。基於此,以上程式碼其實稍作修改就可以很好的執行,只需要在初始化鎖的時候加設定一個屬性。請看清單 2 。

清單 2. 設定互斥鎖 recursive 屬性例項

1

2

3

4

pthread_mutexattr_init(&attr);

    // 設定 recursive 屬性

    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);

    pthread_mutex_init(theMutex,&attr);

因此,建議儘量設定 recursive 屬性以初始化 Linux 的互斥鎖,這樣既可以解決同一執行緒遞迴加鎖的問題,又可以避免很多情況下死鎖的發生。這樣做還有一個額外的好處,就是可以讓 Windows 和 Linux 下讓鎖的表現統一。

注意 Linux 平臺上觸發條件變數的自動復位問題

條件變數的置位和復位有兩種常用模型:第一種模型是當條件變數置位(signaled)以後,如果當前沒有執行緒在等待,其狀態會保持為置位(signaled),直到有等待的執行緒進入被觸發,其狀態才會變為復位(unsignaled),這種模型的採用以 Windows 平臺上的 Auto-set Event 為代表。其狀態變化如圖 1 所示:

圖 1. Windows 的條件變數狀態變化流程

Windows 的條件變數狀態變化流程

第二種模型則是 Linux 平臺的 Pthread 所採用的模型,當條件變數置位(signaled)以後,即使當前沒有任何執行緒在等待,其狀態也會恢復為復位(unsignaled)狀態。其狀態變化如圖 2 所示:

圖 2. Linux 的條件變數狀態變化流程

Linux 的條件變數狀態變化流程

具體來說,Linux 平臺上 Pthread 下的條件變數狀態變化模型是這樣工作的:呼叫 pthread_cond_signal() 釋放被條件阻塞的執行緒時,無論存不存在被阻塞的執行緒,條件都將被重新復位,下一個被條件阻塞的執行緒將不受影響。而對於 Windows,當呼叫 SetEvent 觸發 Auto-reset 的 Event 條件時,如果沒有被條件阻塞的執行緒,那麼條件將維持在觸發狀態,直到有新的執行緒被條件阻塞並被釋放為止。

這種差異性對於那些熟悉 Windows 平臺上的條件變數狀態模型而要開發 Linux 平臺上多執行緒的程式設計師來說可能會造成意想不到的尷尬結果。試想要實現一個旅客坐計程車的程式:旅客在路邊等計程車,呼叫條件等待。計程車來了,將觸發條件,旅客停止等待並上車。一個計程車只能搭載一波乘客,於是我們使用單一觸發的條件變數。這個實現邏輯在第一個模型下即使出租車先到,也不會有什麼問題,其過程如圖 3 所示:

圖 3. 採用 Windows 條件變數模型的計程車例項流程

索引使用的容量要求

然而如果按照這個思路來在 Linux 上來實現,程式碼看起來可能是清單 3 這樣。

清單 3. Linux 計程車案例程式碼例項

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

……

 // 提示計程車到達的條件變數

 pthread_cond_t taxiCond;

 

 // 同步鎖

 pthread_mutex_t taxiMutex;

 

 // 旅客到達等待計程車

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

 

 // 計程車到達

 void * taxi_arrive(void *name) {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;

    pthread_cond_signal(&taxtCond);

    pthread_exit( (void *)0 );

 }

 

 void main() { 

    // 初始化

    taxtCond= PTHREAD_COND_INITIALIZER;

    taxtMutex= PTHREAD_MUTEX_INITIALIZER;

    pthread_t thread;

    pthread_attr_t threadAttr;

    pthread_attr_init(&threadAttr);

 

    pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));

    sleep(1);

 

    return 0;

 }

好的,執行一下,看看結果如清單 4 。

清單 4. 程式結果輸出

1

2

3

4

Taxi Jack arrives.

    Traveler Susan needs a taxi now!

    Taxi Mike arrives.

    Traveler Susan now got a taxi.

其過程如圖 4 所示:

圖 4. 採用 Linux 條件變數模型的計程車例項流程

圖 4. 採用Linux條件變數模型的計程車例項流程

通過對比結果,你會發現同樣的邏輯,在 Linux 平臺上執行的結果卻完全是兩樣。對於在 Windows 平臺上的模型一, Jack 開著計程車到了站臺,觸發條件變數。如果沒顧客,條件變數將維持觸發狀態,也就是說 Jack 停下車在那裡等著。直到 Susan 小姐來了站臺,執行等待條件來找出租車。 Susan 搭上 Jack 的計程車離開,同時條件變數被自動復位。

但是到了 Linux 平臺,問題就來了,Jack 到了站臺一看沒人,觸發的條件變數被直接復位,於是 Jack 排在等待佇列裡面。來遲一秒的 Susan 小姐到了站臺卻看不到在那裡等待的 Jack,只能等待,直到 Mike 開車趕到,重新觸發條件變數,Susan 才上了 Mike 的車。這對於在排隊系統前面的 Jack 是不公平的,而問題癥結是在於 Linux 平臺上條件變數觸發的自動復位引起的一個 Bug 。

條件變數在 Linux 平臺上的這種模型很難說好壞。但是在實際開發中,我們可以對程式碼稍加改進就可以避免這種差異的發生。由於這種差異只發生在觸發沒有被執行緒等待在條件變數的時刻,因此我們只需要掌握好觸發的時機即可。最簡單的做法是增加一個計數器記錄等待執行緒的個數,在決定觸發條件變數前檢查下該變數即可。改進後 Linux 函式如清單 5 所示。

清單 5. Linux 計程車案例程式碼例項

……

 // 提示計程車到達的條件變數

 pthread_cond_t taxiCond;


 // 同步鎖

 pthread_mutex_t taxiMutex;


 // 旅客人數,初始為 0

 int travelerCount=0;


 // 旅客到達等待計程車

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);


    // 提示旅客人數增加

    travelerCount++;

    pthread_cond_wait (&taxiCond, &taxiMutex);

    pthread_mutex_unlock (&taxiMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }


 // 計程車到達

 void * taxi_arrive(void *name)

 {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;


 while(true)

 {

        pthread_mutex_lock(&taxiMutex);



        // 當發現已經有旅客在等待時,才觸發條件變數

        if(travelerCount>0)

        {

            pthread_cond_signal(&taxtCond);

            pthread_mutex_unlock (&taxiMutex);

            break;

        }

        pthread_mutex_unlock (&taxiMutex);

    }

    pthread_exit( (void *)0 );

 }


注意條件返回時互斥鎖的解鎖問題因此我們建議在 Linux 平臺上要出發條件變數之前要檢查是否有等待的執行緒,只有當有執行緒在等待時才對條件變數進行觸發。

在 Linux 呼叫 pthread_cond_wait 進行條件變數等待操作時,我們增加一個互斥變數引數是必要的,這是為了避免執行緒間的競爭和飢餓情況。但是當條件等待返回時候,需要注意的是一定不要遺漏對互斥變數進行解鎖。

Linux 平臺上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函式返回時,互斥鎖 mutex 將處於鎖定狀態。因此之後如果需要對臨界區資料進行重新訪問,則沒有必要對 mutex 就行重新加鎖。但是,隨之而來的問題是,每次條件等待以後需要加入一步手動的解鎖操作。正如前文中乘客等待計程車的 Linux 程式碼如清單 6 所示:

清單 6. 條件變數返回後的解鎖例項

1

2

3

4

5

6

7

8

void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

等待的絕對時間問題

超時是多執行緒程式設計中一個常見的概念。例如,當你在 Linux 平臺下使用 pthread_cond_timedwait() 時就需要指定超時這個引數,以便這個 API 的呼叫者最多隻被阻塞指定的時間間隔。pthread_cond_timedwait() 函式定義

1

2

3

int pthread_cond_timedwait(pthread_cond_t *restrict cond,

              pthread_mutex_t *restrict mutex,

              const struct timespec *restrict abstime);

引數 abstime 在這裡用來表示和超時時間相關的一個引數,但是需要注意的是它所表示的是一個絕對時間,而不是一個時間間隔數值,只有當系統的當前時間達到或者超過 abstime 所表示的時間時,才會觸發超時事件。

 相對時間到絕對時間轉換例項

1

2

3

4

5

6

7

/* get the current time */

    struct timeval now;

    gettimeofday(&now, NULL);

     

    /* add the offset to get timeout value */

    abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;

    abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

Linux 的絕對時間看似簡單明瞭,卻是開發中一個非常隱晦的陷阱。而且一旦你忘了時間轉換,可以想象,等待你的錯誤將是多麼的令人頭疼:如果忘了把相對時間轉換成絕對時間,相當於你告訴系統你所等待的超時時間是過去式的 1970 年 1 月 1 號某個時間段,於是作業系統毫不猶豫馬上送給你一個 timeout 的返回值,然後你會舉著拳頭抱怨為什麼另外一個同步執行緒耗時居然如此之久,並一頭扎進尋找耗時原因的深淵裡。

正確處理 Linux 平臺下的執行緒結束問題

在 Linux 平臺下,當處理執行緒結束時需要注意的一個問題就是如何讓一個執行緒善始善終,讓其所佔資源得到正確釋放。在 Linux 平臺預設情況下,雖然各個執行緒之間是相互獨立的,一個執行緒的終止不會去通知或影響其他的執行緒。但是已經終止的執行緒的資源並不會隨著執行緒的終止而得到釋放,我們需要呼叫 pthread_join() 來獲得另一個執行緒的終止狀態並且釋放該執行緒所佔的資源。 Pthread_join() 函式的定義:

1

int pthread_join(pthread_t th, void **thread_return);

呼叫該函式的執行緒將掛起,等待 th 所表示的執行緒的結束。 thread_return 是指向執行緒 th 返回值的指標。需要注意的是 th 所表示的執行緒必須是 joinable 的,即處於非 detached(遊離)狀態;並且只可以有唯一的一個執行緒對 th 呼叫 pthread_join() 。如果 th 處於 detached 狀態,那麼對 th 的 pthread_join() 呼叫將返回錯誤。

如果你壓根兒不關心一個執行緒的結束狀態,那麼也可以將一個執行緒設定為 detached 狀態,從而來讓作業系統在該執行緒結束時來回收它所佔的資源。將一個執行緒設定為 detached 狀態可以通過兩種方式來實現。一種是呼叫 pthread_detach() 函式,可以將執行緒 th 設定為 detached 狀態。

pthread_detach 函式定義:

1

int pthread_detach(pthread_t th);

另一種方法是在建立執行緒時就將它設定為 detached 狀態,首先初始化一個執行緒屬性變數,然後將其設定為 detached 狀態,最後將它作為引數傳入執行緒建立函式 pthread_create(),這樣所創建出來的執行緒就直接處於 detached 狀態。方法如下。

建立 detach 執行緒程式碼例項:

1

    pthread_t tid;

    pthread_attr_t  attr;

    pthread_attr_init(&attr);

    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

總之為了在使用 Pthread 時避免執行緒的資源線上程結束時不能得到正確釋放,從而避免產生潛在的記憶體洩漏問題,在對待執行緒結束時,要確保該執行緒處於 detached 狀態,否著就需要呼叫 pthread_join() 函式來對其進行資源回收。

總結與補充

本文以上部分詳細介紹了 Linux 的多執行緒程式設計的 5 條高效開發經驗。另外你也可以考慮嘗試其他一些開源類庫來進行執行緒開發。

1. Boost 庫

Boost 庫來自於由 C++ 標準委員會類庫工作組成員發起,致力於為 C++ 開發新的類庫的 Boost 組織。雖然該庫本身並不是針對多執行緒而產生,但是發展至今,其已提供了比較全面的多執行緒程式設計的 API 支援。 Boost 庫對於多執行緒支援的 API 風格上更類似於 Linux 的 Pthread 庫,差別在於其將執行緒,互斥鎖,條件等執行緒開發概念都封裝成了 C++ 類,以方便開發呼叫。 Boost 庫目前對跨平臺支援的很不錯,不僅支援 Windows 和 Linux ,還支援各種商用的 Unix 版本。如果開發者想使用高穩定性的統一執行緒程式設計介面減輕跨平臺開發的難度, Boost 庫將是首選。

2. ACE

ACE 全稱是 ADAPTIVE Communication Environment,它是一個免費的,開源的,面向物件的工具框架,用以開發併發訪問的軟體。由於 ACE 最初是面向網路服務端的程式設計開發,因此對於執行緒開發的工具庫它也能提供很全面的支援。其支援的平臺也很全面,包括 Windows,Linux 和各種版本 Unix 。 ACE 的唯一問題是如果僅僅是用於執行緒程式設計,其似乎顯得有些過於重量級。而且其較複雜的配置也讓其部署對初學者而言並非易事。