Linux下線程同步的幾種方法


Linux下提供了多種方式來處理線程同步,最常用的是互斥鎖、條件變量和信號量。

一、互斥鎖(mutex)
  鎖機制是同一時刻只允許一個線程執行一個關鍵部分的代碼。

 1. 初始化鎖
  int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
   其中參數 mutexattr 用於指定鎖的屬性(見下),如果為NULL則使用缺省屬性。
   互斥鎖的屬性在創建鎖的時候指定,在LinuxThreads實現中僅有一個鎖類型屬性,不同的鎖類型在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前有四個值可供選擇:
   (1)PTHREAD_MUTEX_TIMED_NP,這是缺省值,也就是普通鎖。當一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,並在解鎖后按優先級獲得鎖。這種鎖策略保證了資源分配的公平性。
   (2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套鎖,允許同一個線程對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同線程請求,則在加鎖線程解鎖時重新競爭。
   (3)PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個線程請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP類型動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
   (4)PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖類型,僅等待解鎖后重新競爭。

 2. 阻塞加鎖
  int pthread_mutex_lock(pthread_mutex *mutex);
 3. 非阻塞加鎖
   int pthread_mutex_trylock( pthread_mutex_t *mutex);
   該函數語義與 pthread_mutex_lock() 類似,不同的是在鎖已經被占據時返回 EBUSY 而不是掛起等待。
 4. 解鎖(要求鎖是lock狀態,並且由加鎖線程解鎖)
  int pthread_mutex_unlock(pthread_mutex *mutex);
 5. 銷毀鎖(此時鎖必需unlock狀態,否則返回EBUSY)
  int pthread_mutex_destroy(pthread_mutex *mutex);
  示例代碼:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5.  
  6. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  7.  
  8. int gn;
  9.  
  10. void* thread(void *arg)
  11. {
  12. printf("thread's ID is %d\n",pthread_self());
  13. pthread_mutex_lock(&mutex);
  14. gn = 12;
  15. printf("Now gn = %d\n",gn);
  16. pthread_mutex_unlock(&mutex);
  17. return NULL;
  18. }
  19.  
  20. int main()
  21. {
  22. pthread_t id;
  23. printf("main thread's ID is %d\n",pthread_self());
  24. gn = 3;
  25. printf("In main func, gn = %d\n",gn);
  26. if (!pthread_create(&id, NULL, thread, NULL))
  27. {
  28. printf("Create thread success!\n");
  29. } else
  30. {
  31. printf("Create thread failed!\n");
  32. }
  33. pthread_join(id, NULL);
  34. pthread_mutex_destroy(&mutex);
  35.  
  36. return 0;
  37.  
  38. }

 

二、條件變量(cond)

  條件變量是利用線程間共享全局變量進行同步的一種機制。條件變量上的基本操作有:觸發條件(當條件變為 true 時);等待條件,掛起線程直到其他線程觸發條件。
   
   1. 初始化條件變量
     int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
      盡管POSIX標准中為條件變量定義了屬性,但在Linux中沒有實現,因此cond_attr值通常為NULL,且被忽略。
   2. 有兩個等待函數 
      (1)無條件等待

         int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
      (2)計時等待
         int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
          如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統調用相同意義的絕對時間形式出現,0表示格林尼治時間1970年1月1日0時0分0秒。
 
      無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 請求)競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖(PTHREAD_MUTEX_ADAPTIVE_NP),且在調用pthread_cond_wait()前必須由本線程加鎖(pthread_mutex_lock()),而在更新條件等待隊列以前,mutex保持鎖定狀態,並在線程掛起進入等待前解鎖。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。

   3. 激發條件
     (1)激活一個等待該條件的線程(存在多個等待線程時按入隊順序激活其中一個)
  
         int pthread_cond_signal(pthread_cond_t *cond);
     (2)激活所有等待線程
      int pthread_cond_broadcast(pthread_cond_t *cond); 

   4. 銷毀條件變量
     int pthread_cond_destroy(pthread_cond_t *cond);
      只有在沒有線程在該條件變量上等待的時候才能銷毀這個條件變量,否則返回EBUSY

說明:
  1. pthread_cond_wait 自動解鎖互斥量(如同執行了pthread_unlock_mutex),並等待條件變量觸發。這時線程掛起,不占用CPU時間,直到條件變量被觸發(變量為ture)。在調用 pthread_cond_wait之前,應用程序必須加鎖互斥量。pthread_cond_wait函數返回前,自動重新對互斥量加鎖(如同執行了pthread_lock_mutex)。

  2. 互斥量的解鎖和在條件變量上掛起都是自動進行的。因此,在條件變量被觸發前,如果所有的線程都要對互斥量加鎖,這種機制可保證在線程加鎖互斥量和進入等待條件變量期間,條件變量不被觸發。條件變量要和互斥量相聯結,以避免出現條件競爭——個線程預備等待一個條件變量,當它在真正進入等待之前,另一個線程恰好觸發了該條件(條件滿足信號有可能在測試條件和調用pthread_cond_wait函數(block)之間被發出,從而造成無限制的等待)。

  3. 條件變量函數不是異步信號安全的,不應當在信號處理程序中進行調用。特別要注意,如果在信號處理程序中調用 pthread_cond_signal 或 pthread_cond_boardcast 函數,可能導致調用線程死鎖

示例代碼1:

 

  1. #include <stdio.h>
  2. #include <pthread.h>
  3. #include "stdlib.h"
  4. #include "unistd.h"
  5.  
  6. pthread_mutex_t mutex;
  7. pthread_cond_t cond;
  8.  
  9. void hander(void *arg)
  10. {
  11. free(arg);
  12. ( void)pthread_mutex_unlock(&mutex);
  13. }
  14.  
  15. void *thread1(void *arg)
  16. {
  17. pthread_cleanup_push(hander, &mutex);
  18. while(1)
  19. {
  20. printf("thread1 is running\n");
  21. pthread_mutex_lock(&mutex);
  22. pthread_cond_wait(&cond,&mutex);
  23. printf("thread1 applied the condition\n");
  24. pthread_mutex_unlock(&mutex);
  25. sleep( 4);
  26. }
  27. pthread_cleanup_pop( 0);
  28. }
  29.  
  30. void *thread2(void *arg)
  31. {
  32. while(1)
  33. {
  34. printf("thread2 is running\n");
  35. pthread_mutex_lock(&mutex);
  36. pthread_cond_wait(&cond,&mutex);
  37. printf("thread2 applied the condition\n");
  38. pthread_mutex_unlock(&mutex);
  39. sleep( 1);
  40. }
  41. }
  42.  
  43. int main()
  44. {
  45. pthread_t thid1,thid2;
  46. printf("condition variable study!\n");
  47. pthread_mutex_init(&mutex, NULL);
  48. pthread_cond_init(&cond, NULL);
  49. pthread_create(&thid1, NULL,thread1,NULL);
  50. pthread_create(&thid2, NULL,thread2,NULL);
  51.  
  52. sleep( 1);
  53.  
  54. do{
  55. pthread_cond_signal(&cond);
  56. } while(1);
  57.  
  58. sleep( 20);
  59. pthread_exit( 0);
  60.  
  61. return 0;
  62. }

示例代碼2:

 

  1. #include <pthread.h>
  2. #include <unistd.h>
  3. #include "stdio.h"
  4. #include "stdlib.h"
  5.  
  6. static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
  7. static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  8.  
  9. struct node
  10. {
  11. int n_number;
  12. struct node *n_next;
  13. }*head = NULL;
  14.  
  15. static void cleanup_handler(void *arg)
  16. {
  17. printf("Cleanup handler of second thread.\n");
  18. free(arg);
  19. ( void)pthread_mutex_unlock(&mtx);
  20. }
  21.  
  22. static void *thread_func(void *arg)
  23. {
  24. struct node *p = NULL;
  25. pthread_cleanup_push(cleanup_handler, p);
  26.  
  27. while (1)
  28. {
  29. // 這個mutex主要是用來保證pthread_cond_wait的並發性。
  30. pthread_mutex_lock(&mtx);
  31. while (head == NULL)
  32. {
  33. /* 這個while要特別說明一下,單個pthread_cond_wait功能很完善,為何
  34. * 這里要有一個while (head == NULL)呢?因為pthread_cond_wait里的線
  35. * 程可能會被意外喚醒,如果這個時候head != NULL,則不是我們想要的情況。
  36. * 這個時候,應該讓線程繼續進入pthread_cond_wait
  37. * pthread_cond_wait會先解除之前的pthread_mutex_lock鎖定的mtx,
  38. * 然后阻塞在等待對列里休眠,直到再次被喚醒(大多數情況下是等待的條件成立
  39. * 而被喚醒,喚醒后,該進程會先鎖定先pthread_mutex_lock(&mtx);,再讀取資源
  40. * 用這個流程是比較清楚的。*/
  41.  
  42. pthread_cond_wait(&cond, &mtx);
  43. p = head;
  44. head = head->n_next;
  45. printf("Got %d from front of queue\n", p->n_number);
  46. free(p);
  47. }
  48.  
  49. pthread_mutex_unlock(&mtx); // 臨界區數據操作完畢,釋放互斥鎖。
  50. }
  51.  
  52. pthread_cleanup_pop( 0);
  53.  
  54. return 0;
  55.  
  56. }
  57.  
  58. int main(void)
  59. {
  60. pthread_t tid;
  61. int i;
  62. struct node *p;
  63.  
  64. /* 子線程會一直等待資源,類似生產者和消費者,但是這里的消費者可以是多個消費者,
  65. * 而不僅僅支持普通的單個消費者,這個模型雖然簡單,但是很強大。*/
  66.  
  67. pthread_create(&tid, NULL, thread_func, NULL);
  68.  
  69. sleep( 1);
  70.  
  71. for (i = 0; i < 10; i++)
  72. {
  73. p = (struct node*) malloc(sizeof(struct node));
  74. p->n_number = i;
  75. pthread_mutex_lock(&mtx); // 需要操作head這個臨界資源,先加鎖。
  76.  
  77. p->n_next = head;
  78. head = p;
  79.  
  80. pthread_cond_signal(&cond);
  81.  
  82. pthread_mutex_unlock(&mtx); //解鎖
  83.  
  84. sleep( 1);
  85. }
  86.  
  87. printf("thread 1 wanna end the line.So cancel thread 2.\n");
  88.  
  89. /* 關於pthread_cancel,有一點額外的說明,它是從外部終止子線程,子線程會在最近的取消點,
  90. * 退出線程,而在我們的代碼里,最近的取消點肯定就是pthread_cond_wait()了。*/
  91.  
  92. pthread_cancel(tid);
  93.  
  94. pthread_join(tid, NULL);
  95.  
  96. printf("All done -- exiting\n");
  97.  
  98. return 0;
  99. }

可以看出,等待條件變量信號的用法約定一般是這樣的:
...
pthread_mutex_lock(&mutex);
...
pthread_cond_wait (&cond, &mutex);
...
pthread_mutex_unlock (&mutex);
...

相信很多人都會有這個疑問:為什么pthread_cond_wait需要的互斥鎖不在函數內部定義,而要使用戶定義的呢?現在沒有時間研究 pthread_cond_wait 的源代碼,帶着這個問題對條件變量的用法做如下猜測,希望明白真相看過源代碼的朋友不吝指正。

1. pthread_cond_wait 和 pthread_cond_timewait 函數為什么需要互斥鎖?因為:條件變量是線程同步的一種方法,這兩個函數又是等待信號的函數,函數內部一定有須要同步保護的數據。
2. 使用用戶定義的互斥鎖而不在函數內部定義的原因是:無法確定會有多少用戶使用條件變量,所以每個互斥鎖都須要動態定義,而且管理大量互斥鎖的開銷太大,使用用戶定義的即靈活又方便,符合UNIX哲學的編程風格(隨便推薦閱讀《UNIX編程哲學》這本好書!)。
3. 好了,說完了1和2,我們來自由猜測一下 pthread_cond_wait 函數的內部結構吧:
  int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
   {
      if(沒有條件信號)
      {
         (1)pthread_mutex_unlock (mutex); // 因為用戶在函數外面已經加鎖了(這是使用約定),但是在沒有信號的情況下為了讓其他線程也能等待cond,必須解鎖。
         (2) 阻塞當前線程,等待條件信號(當然應該是類似於中斷觸發的方式等待,而不是軟件輪詢的方式等待)... 有信號就繼續執行后面。
         (3) pthread_mutex_lock (mutex); // 因為用戶在函數外面要解鎖(這也是使用約定),所以要與1呼應加鎖,保證用戶感覺依然是自己加鎖、自己解鎖。
      }      
      ...
  }

三、 信號量


 如同進程一樣,線程也可以通過信號量來實現通信,雖然是輕量級的。
   線程使用的基本信號量函數有四個:

  #include <semaphore.h>

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

      參數:
      sem - 指定要初始化的信號量;
      pshared - 信號量 sem 的共享選項,linux只支持0,表示它是當前進程的局部信號量;
      value - 信號量 sem 的初始值。

      2. 信號量值加1
      給參數sem指定的信號量值加1。
     int sem_post(sem_t *sem);

     3. 信號量值減1
      給參數sem指定的信號量值減1。
     int sem_wait(sem_t *sem);
      如果sem所指的信號量的數值為0,函數將會等待直到有其它線程使它不再是0為止。

     4. 銷毀信號量
    銷毀指定的信號量。
  int sem_destroy(sem_t *sem);

  示例代碼:

 

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. #include <semaphore.h>
  6. #include <errno.h>
  7.  
  8. #define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}
  9.  
  10. typedef struct _PrivInfo
  11. {
  12. sem_t s1;
  13. sem_t s2;
  14. time_t end_time;
  15. }PrivInfo;
  16.  
  17. static void info_init (PrivInfo* prifo);
  18. static void info_destroy (PrivInfo* prifo);
  19. static void* pthread_func_1 (PrivInfo* prifo);
  20. static void* pthread_func_2 (PrivInfo* prifo);
  21.  
  22. int main (int argc, char** argv)
  23. {
  24. pthread_t pt_1 = 0;
  25. pthread_t pt_2 = 0;
  26. int ret = 0;
  27. PrivInfo* prifo = NULL;
  28. prifo = (PrivInfo* ) malloc (sizeof (PrivInfo));
  29.  
  30. if (prifo == NULL)
  31. {
  32. printf ("[%s]: Failed to malloc priv.\n");
  33. return -1;
  34. }
  35.  
  36. info_init (prifo);
  37. ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
  38. if (ret != 0)
  39. {
  40. perror ( "pthread_1_create:");
  41. }
  42.  
  43. ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
  44. if (ret != 0)
  45. {
  46. perror ( "pthread_2_create:");
  47. }
  48.  
  49. pthread_join (pt_1, NULL);
  50. pthread_join (pt_2, NULL);
  51. info_destroy (prifo);
  52. return 0;
  53. }
  54.  
  55. static void info_init (PrivInfo* prifo)
  56. {
  57. return_if_fail (prifo != NULL);
  58. prifo->end_time = time( NULL) + 10;
  59. sem_init (&prifo->s1, 0, 1);
  60. sem_init (&prifo->s2, 0, 0);
  61. return;
  62. }
  63.  
  64. static void info_destroy (PrivInfo* prifo)
  65. {
  66. return_if_fail (prifo != NULL);
  67. sem_destroy (&prifo->s1);
  68. sem_destroy (&prifo->s2);
  69. free (prifo);
  70. prifo = NULL;
  71. return;
  72. }
  73.  
  74. static void* pthread_func_1 (PrivInfo* prifo)
  75. {
  76. return_if_fail (prifo != NULL);
  77. while (time(NULL) < prifo->end_time)
  78. {
  79. sem_wait (&prifo->s2);
  80. printf ("pthread1: pthread1 get the lock.\n");
  81. sem_post (&prifo->s1);
  82. printf ("pthread1: pthread1 unlock\n");
  83. sleep ( 1);
  84. }
  85. return;
  86. }
  87.  
  88. static void* pthread_func_2 (PrivInfo* prifo)
  89. {
  90. return_if_fail (prifo != NULL);
  91. while (time (NULL) < prifo->end_time)
  92. {
  93. sem_wait (&prifo->s1);
  94. printf ("pthread2: pthread2 get the unlock.\n");
  95. sem_post (&prifo->s2);
  96. printf ("pthread2: pthread2 unlock.\n");
  97. sleep ( 1);
  98. }
  99. return;
  100. }

 

四、異步信號 


由於LinuxThreads是在核外使用核內輕量級進程實現的線程,所以基於內核的異步信號操作對於線程也是有效的。但同時,由於異步信號總是實際發往某個進程,所以無法實現POSIX標准所要求的"信號到達某個進程,然后再由該進程將信號分發到所有沒有阻塞該信號的線程中"原語,而是只能影響到其中一個線程。

POSIX異步信號同時也是一個標准C庫提供的功能,主要包括信號集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、信號處理函數安裝(sigaction())、信號阻塞控制(sigprocmask())、被阻塞信號查詢(sigpending())、信號等待(sigsuspend())等,它們與發送信號的kill()等函數配合就能實現進程間異步信號功能。LinuxThreads圍繞線程封裝了sigaction()何raise(),本節集中討論LinuxThreads中擴展的異步信號函數,包括pthread_sigmask()、pthread_kill()和sigwait()三個函數。毫無疑問,所有POSIX異步信號函數對於線程都是可用的。

int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask) 
設置線程的信號屏蔽碼,語義與sigprocmask()相同,但對不允許屏蔽的Cancel信號和不允許響應的Restart信號進行了保護。被屏蔽的信號保存在信號隊列中,可由sigpending()函數取出。

int pthread_kill(pthread_t thread, int signo) 
向thread號線程發送signo信號。實現中在通過thread線程號定位到對應進程號以后使用kill()系統調用完成發送。 

int sigwait(const sigset_t *set, int *sig) 
掛起線程,等待set中指定的信號之一到達,並將到達的信號存入*sig中。POSIX標准建議在調用sigwait()等待信號以前,進程中所有線程都應屏蔽該信號,以保證僅有sigwait()的調用者獲得該信號,因此,對於需要等待同步的異步信號,總是應該在創建任何線程以前調用pthread_sigmask()屏蔽該信號的處理。而且,調用sigwait()期間,原來附接在該信號上的信號處理函數不會被調用。

如果在等待期間接收到Cancel信號,則立即退出等待,也就是說sigwait()被實現為取消點。 

五、 其他同步方式 


除了上述討論的同步方式以外,其他很多進程間通信手段對於LinuxThreads也是可用的,比如基於文件系統的IPC(管道、Unix域Socket等)、消息隊列(Sys.V或者Posix的)、System V的信號燈等。只有一點需要注意,LinuxThreads在核內是作為共享存儲區、共享文件系統屬性、共享信號處理、共享文件描述符的獨立進程看待的。

 

條件變量與互斥鎖、信號量的區別

       1.互斥鎖必須總是由給它上鎖的線程解鎖,信號量的掛出即不必由執行過它的等待操作的同一進程執行。一個線程可以等待某個給定信號燈,而另一個線程可以掛出該信號燈。

       2.互斥鎖要么鎖住,要么被解開(二值狀態,類型二值信號量)。

       3.由於信號量有一個與之關聯的狀態(它的計數值),信號量掛出操作總是被記住。然而當向一個條件變量發送信號時,如果沒有線程等待在該條件變量上,那么該信號將丟失。

       4.互斥鎖是為了上鎖而設計的,條件變量是為了等待而設計的,信號燈即可用於上鎖,也可用於等待,因而可能導致更多的開銷和更高的復雜性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM