天天看點

pthread_mutex_t的使用

在工作中需要使用pthread,對于加鎖操作下邊文章介紹的比較好,并且作者還有一些關于pthread的介紹不錯:

原文位址:http://blog.chinaunix.net/uid-26921272-id-3203633.html

POSIX 線程是提高代碼響應和性能的有力手段。在此三部分系列文章的第二篇中,DanielRobbins将說明,如何使用被稱為互斥對象的靈巧小玩意,來保護線程代碼中共享資料結構的完整性。

互斥我吧!

在 前一篇文章中,談到了會導緻異常結果的線程代碼。兩個線程分别對同一個全局變量進行了二十次加一。變量的值最後應該是 40,但最終值卻是21。這是怎麼回事呢?因為一個線程不停地“取消”了另一個線程執行的加一操作,是以産生這個問題。現在讓我們來檢視改正後的代碼,它使用互斥對象(mutex)來解決該問題:

thread3.c

#include <pthread.h>

#include <stdlib.h>

#include <unistd.h>

#include <stdio.h>i

nt myglobal;

pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;

void *thread_function(void *arg)

{

     int i,j;

     for ( i=0; i<20; i++)

     {

          pthread_mutex_lock(&mymutex);

          j=myglobal; j=j+1;

          printf(".");

          fflush(stdout);

          sleep(1);

          myglobal=j;

          pthread_mutex_unlock(&mymutex);

     }

     return NULL;

}

int main(void)

{

     pthread_t mythread;

     int i;

     if ( pthread_create( &mythread, NULL, thread_function, NULL) )

     {

          printf("error creating thread.");

          abort();

     }

     for ( i=0; i<20; i++)

     {

          pthread_mutex_lock(&mymutex);

          myglobal=myglobal+1;

          pthread_mutex_unlock(&mymutex);

          printf("o");

          fflush(stdout);

          sleep(1);

     }

     if ( pthread_join ( mythread, NULL ) )

     {

          printf("error joining thread.");

          abort();

     }

     printf("\nmyglobal equals %d\n",myglobal);

     exit(0);

}

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

解讀一下

如果将這段代碼與 前一篇文章中給出的版本作一個比較,就會注意到增加了 pthread_mutex_lock() 和 pthread_mutex_unlock()函數調用。線上程程式中這些調用執行了不可或缺的功能。他們提供了一種互相排斥的方法(互斥對象即由此得名)。兩個線程不能同時對同一個互斥對象加鎖。

互斥對象是這樣工作的。如果線程 a 試圖鎖定一個互斥對象,而此時線程 b 已鎖定了同一個互斥對象時,線程 a就将進入睡眠狀态。一旦線程 b 釋放了互斥對象(通過 pthread_mutex_unlock() 調用),線程 a就能夠鎖定這個互斥對象(換句話說,線程 a 就将從 pthread_mutex_lock()函數調用中傳回,同時互斥對象被鎖定)。同樣地,當線程 a 正鎖定互斥對象時,如果線程 c 試圖鎖定互斥對象的話,線程 c也将臨時進入睡眠狀态。對已鎖定的互斥對象上調用 pthread_mutex_lock()的所有線程都将進入睡眠狀态,這些睡眠的線程将“排隊”通路這個互斥對象。

通常使用 pthread_mutex_lock() 和 pthread_mutex_unlock()來保護資料結構。這就是說,通過線程的鎖定和解鎖,對于某一資料結構,確定某一時刻隻能有一個線程能夠通路它。可以推測到,當線程試圖鎖定一個未加鎖的互斥對象時,POSIX線程庫将同意鎖定,而不會使線程進入睡眠狀态。

請看這幅輕松的漫畫,四個小精靈重制了最近一次 pthread_mutex_lock()調用的一個場面。

pthread_mutex_t的使用

圖中,鎖定了互斥對象的線程能夠存取複雜的資料結構,而不必擔心同時會有其它線程幹擾。那個資料結構實際上是“當機”了,直到互斥對象被解鎖為止。pthread_mutex_lock()和 pthread_mutex_unlock()函數調用,如同“在施工中”标志一樣,将正在修改和讀取的某一特定共享資料包圍起來。這兩個函數調用的作用就是警告其它線程,要它們繼續睡眠并等待輪到它們對互斥對象加鎖。當然,除非在每個 對特定資料結構進行讀寫操作的語句前後,都分别放上 pthread_mutex_lock() 和pthread_mutext_unlock() 調用,才會出現這種情況。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

為什麼要用互斥對象?

聽上去很有趣,但究竟為什麼要讓線程睡眠呢?要知道,線程的主要優點不就是其具有獨立工作、更多的時候是同時工作的能力嗎?是的,确實是這樣。然而,每個重要的線程程式都需要使用某些互斥對象。讓我們再看一下示例程式以便了解原因所在。

請看 thread_function(),循環中一開始就鎖定了互斥對象,最後才将它解鎖。在這個示例程式中,mymutex用來保護 myglobal 的值。仔細檢視 thread_function(),加一代碼把 myglobal複制到一個局部變量,對局部變量加一,睡眠一秒鐘,在這之後才把局部變量的值傳回給 myglobal。不使用互斥對象時,即使主線程在thread_function() 線程睡眠一秒鐘期間内對 myglobal 加一,thread_function()蘇醒後也會覆寫主線程所加的值。使用互斥對象能夠保證這種情形不會發生。(您也許會想到,我增加了一秒鐘延遲以觸發不正确的結果。把局部變量的值賦給myglobal 之前,實際上沒有什麼真正理由要求 thread_function()睡眠一秒鐘。)使用互斥對象的新程式産生了期望的結果:

$ ./thread3o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooomyglobal equals 40

為了進一步探索這個極為重要的概念,讓我們看一看程式中進行加一操作的代碼:

thread_function() 加一代碼: j=myglobal; j=j+1; printf("."); fflush(stdout); sleep(1); myglobal=j;主線程加一代碼: myglobal=myglobal+1;

如果代碼是位于單線程程式中,可以預期 thread_function()代碼将完整執行。接下來才會執行主線程代碼(或者是以相反的順序執行)。在不使用互斥對象的線程程式中,代碼可能(幾乎是,由于調用了sleep() 的緣故)以如下的順序執行:

thread_function() 線程 主線程 j=myglobal; j=j+1; printf("."); fflush(stdout); sleep(1); myglobal=myglobal+1; myglobal=j;

當代碼以此特定順序執行時,将覆寫主線程對 myglobal的修改。程式結束後,就将得到不正确的值。如果是在操縱指針的話,就可能産生段錯誤。注意到 thread_function()線程按順序執行了它的所有指令。看來不象是 thread_function()有什麼次序颠倒。問題是,同一時間内,另一個線程對同一資料結構進行了另一個修改。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

線程内幕 1

在解釋如何确定在何處使用互斥對象之前,先來深入了解一下線程的内部工作機制。請看第一個例子:

假設主線程将建立三個新線程:線程 a、線程 b 和線程 c。假定首先建立線程 a,然後是線程 b,最後建立線程 c。

pthread_create( &thread_a, NULL, thread_function, NULL); pthread_create( &thread_b, NULL, thread_function, NULL); pthread_create( &thread_c, NULL, thread_function, NULL);

在第一個 pthread_create() 調用完成後,可以假定線程 a 不是已存在就是已結束并停止。第二個pthread_create() 調用後,主線程和線程 b 都可以假定線程 a 存在(或已停止)。

然而,就在第二個 create() 調用傳回後,主線程無法假定是哪一個線程(a 或b)會首先開始運作。雖然兩個線程都已存在,線程 CPU 時間片的配置設定取決于核心和線程庫。至于誰将首先運作,并沒有嚴格的規則。盡管線程a 更有可能線上程 b 之前開始執行,但這并無保證。對于多處理器系統,情況更是如此。如果編寫的代碼假定線上程 b開始執行之前實際上執行線程 a 的代碼,那麼,程式最終正确運作的機率是 99%。或者更糟糕,程式在您的機器上 100%地正确運作,而在您客戶的四處理器伺服器上正确運作的機率卻是零。

從這個例子還可以得知,線程庫保留了每個單獨線程的代碼執行順序。換句話說,實際上那三個 pthread_create()調用将按它們出現的順序執行。從主線程上來看,所有代碼都是依次執行的。有時,可以利用這一點來優化部分線程程式。例如,在上例中,線程 c就可以假定線程 a 和線程 b 不是正在運作就是已經終止。它不必擔心存在還沒有建立線程 a 和線程 b的可能性。可以使用這一邏輯來優化線程程式。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

線程内幕 2

現在來看另一個假想的例子。假設有許多線程,他們都正在執行下列代碼:

myglobal=myglobal+1;

那麼,是否需要在加一操作語句前後分别鎖定和解鎖互斥對象呢?也許有人會說“不”。編譯器極有可能把上述指派語句編譯成一條機器指令。大家都知道,不可能"半途"中斷一條機器指令。即使是硬體中斷也不會破壞機器指令的完整性。基于以上考慮,很可能傾向于完全省略pthread_mutex_lock() 和 pthread_mutex_unlock() 調用。不要這樣做。

我在說廢話嗎?不完全是這樣。首先,不應該假定上述指派語句一定會被編譯成一條機器指令,除非親自驗證了機器代碼。即使插入某些内嵌彙編語句以確定加一操作的完整執行――甚至,即使是自己動手寫編譯器!--仍然可能有問題。

答案在這裡。使用單條内嵌彙編操作碼在單處理器系統上可能不會有什麼問題。每個加一操作都将完整地進行,并且多半會得到期望的結果。但是多處理器系統則截然不同。在多CPU 機器上,兩個單獨的處理器可能會在幾乎同一時刻(或者,就在同一時刻)執行上述指派語句。不要忘了,這時對記憶體的修改需要先從 L1寫入 L2 高速緩存、然後才寫入主存。(SMP 機器并不隻是增加了處理器而已;它還有用來仲裁對 RAM存取的特殊硬體。)最終,根本無法搞清在寫入主存的競争中,哪個 CPU将會"勝出"。要産生可預測的代碼,應使用互斥對象。互斥對象将插入一道"記憶體關卡",由它來確定對主存的寫入按照線程鎖定互斥對象的順序進行。

考慮一種以 32 位塊為機關更新主存的 SMP 體系結構。如果未使用互斥對象就對一個 64 位整數進行加一操作,整數的最高 4位位元組可能來自一個 CPU,而其它 4 個位元組卻來自另一CPU。糟糕吧!最糟糕的是,使用差勁的技術,您的程式在重要客戶的系統上有可能不是很長時間才崩潰一次,就是早上三點鐘就崩潰。DavidR. Butenhof 在他的《POSIX 線程程式設計》(請參閱本文末尾的 參考資料部分)一書中,讨論了由于未使用互斥對象而将産生的種種情況。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

許多互斥對象

如果放置了過多的互斥對象,代碼就沒有什麼并發性可言,運作起來也比單線程解決方案慢。如果放置了過少的互斥對象,代碼将出現奇怪和令人尴尬的錯誤。幸運的是,有一個中間立場。首先,互斥對象是用于串行化存取*共享資料*。不要對非共享資料使用互斥對象,并且,如果程式邏輯確定任何時候都隻有一個線程能存取特定資料結構,那麼也不要使用互斥對象。

其次,如果要使用共享資料,那麼在讀、寫共享資料時都應使用互斥對象。用 pthread_mutex_lock() 和pthread_mutex_unlock()把讀寫部分保護起來,或者在程式中不固定的地方随機使用它們。學會從一個線程的角度來審視代碼,并確定程式中每一個線程對記憶體的觀點都是一緻和合适的。為了熟悉互斥對象的用法,最初可能要花好幾個小時來編寫代碼,但是很快就會習慣并且*也*不必多想就能夠正确使用它們。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

使用調用:初始化

現在該來看看使用互斥對象的各種不同方法了。讓我們從初始化開始。在 thread3.c 示例中,我們使用了靜态初始化方法。這需要聲明一個 pthread_mutex_t 變量,并賦給它常數PTHREAD_MUTEX_INITIALIZER:

pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;

很簡單吧。但是還可以動态地建立互斥對象。當代碼使用 malloc()配置設定一個新的互斥對象時,使用這種動态方法。此時,靜态初始化方法是行不通的,并且應當使用例程pthread_mutex_init():

int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr)

正如所示,pthread_mutex_init接受一個指針作為參數以初始化為互斥對象,該指針指向一塊已配置設定好的記憶體區。第二個參數,可以接受一個可選的pthread_mutexattr_t 指針。這個結構可用來設定各種互斥對象屬性。但是通常并不需要這些屬性,是以正常做法是指定NULL。

一旦使用 pthread_mutex_init() 初始化了互斥對象,就應使用 pthread_mutex_destroy()消除它。pthread_mutex_destroy() 接受一個指向 pthread_mutext_t的指針作為參數,并釋放建立互斥對象時配置設定給它的任何資源。請注意, pthread_mutex_destroy() 不會釋放用來存儲 pthread_mutex_t的記憶體。釋放自己的記憶體完全取決于您。還必須注意一點,pthread_mutex_init() 和pthread_mutex_destroy() 成功時都傳回零。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

使用調用:鎖定

pthread_mutex_lock(pthread_mutex_t *mutex)

pthread_mutex_lock()接受一個指向互斥對象的指針作為參數以将其鎖定。如果碰巧已經鎖定了互斥對象,調用者将進入睡眠狀态。函數傳回時,将喚醒調用者(顯然)并且調用者還将保留該鎖。函數調用成功時傳回零,失敗時傳回非零的錯誤代碼。

pthread_mutex_unlock(pthread_mutex_t *mutex)

pthread_mutex_unlock() 與 pthread_mutex_lock()相配合,它把線程已經加鎖的互斥對象解鎖。始終應該盡快對已加鎖的互斥對象進行解鎖(以提高性能)。并且絕對不要對您未保持鎖的互斥對象進行解鎖操作(否則,pthread_mutex_unlock()調用将失敗并帶一個非零的 EPERM 傳回值)。

pthread_mutex_trylock(pthread_mutex_t *mutex)

當線程正在做其它事情的時候(由于互斥對象目前是鎖定的),如果希望鎖定互斥對象,這個調用就相當友善。調用pthread_mutex_trylock()時将嘗試鎖定互斥對象。如果互斥對象目前處于解鎖狀态,那麼您将獲得該鎖并且函數将傳回零。然而,如果互斥對象已鎖定,這個調用也不會阻塞。當然,它會傳回非零的EBUSY 錯誤值。然後可以繼續做其它事情,稍後再嘗試鎖定。

pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
pthread_mutex_t的使用
回頁首

等待條件發生

互斥對象是線程程式必需的工具,但它們并非萬能的。例如,如果線程正在等待共享資料内某個條件出現,那會發生什麼呢?代碼可以反複對互斥對象鎖定和解鎖,以檢查值的任何變化。同時,還要快速将互斥對象解鎖,以便其它線程能夠進行任何必需的更改。這是一種非常可怕的方法,因為線程需要在合理的時間範圍内頻繁地循環檢測變化。

在每次檢查之間,可以讓調用線程短暫地進入睡眠,比如睡眠三秒鐘,但是是以線程代碼就無法最快作出響應。真正需要的是這樣一種方法,當線程在等待滿足某些條件時使線程進入睡眠狀态。一旦條件滿足,還需要一種方法以喚醒因等待滿足特定條件而睡眠的線程。如果能夠做到這一點,線程代碼将是非常高效的,并且不會占用寶貴的互斥對象鎖。這正是POSIX 條件變量能做的事!

而 POSIX條件變量将是我下一篇文章的主題,其中将說明如何正确使用條件變量。到那時,您将擁有了建立複雜線程程式所需的全部資源,那些線程程式可以模拟從業人員、裝配線等等。既然您已經越來越熟悉線程,我将在下一篇文章中加快進度。這樣,在下一篇文章的結尾就能放上一個相對複雜的線程程式。說到等到條件産生,下次再見!

繼續閱讀