多線程編程

Linux互斥鎖、條件變量和信號量

進行多線程編程,最應該注意的就是那些共享的數據,因爲無法知道哪個線程會在哪個時候對它進行操作,也無法得知哪個線程會先運行,哪個線程會後運行。所以,要對這些資源進行合理的分配和正確的使用。在Linux下,提供了互斥鎖、條件變量和信號量來對共享資源進行保護。

一、互斥鎖
互斥鎖,是一種信號量,常用來防止兩個進程或線程在同一時刻訪問相同的共享資源。
需要的頭文件:pthread.h
互斥鎖標識符:pthread_mutex_t


如果一個線程已經給一個互斥量上鎖了,後來在操作的過程中又再次調用了該上鎖的操作,那麼該線程將會無限阻塞在這個地方,從而導致死鎖。這就需要互斥量的屬性。

互斥量分爲下面三種:
1、快速型。這種類型也是默認的類型。該線程的行爲正如上面所說的。
2、遞歸型。如果遇到我們上面所提到的死鎖情況,同一線程循環給互斥量上鎖,那麼系統將會知道該上鎖行爲來自同一線程,那麼就會同意線程給該互斥量上鎖。
3、錯誤檢測型。如果該互斥量已經被上鎖,那麼後續的上鎖將會失敗而不會阻塞,pthread_mutex_lock()操作將會返回EDEADLK。


前面我們提到在調用pthread_mutex_lock()的時候,如果此時mutex已經被其他線程上鎖,那麼該操作將會一直阻塞在這個地方。如果我們此時不想一直阻塞在這個地方,那麼可以調用下面函數:pthread_mutex_trylock。
如果此時互斥量沒有被上鎖,那麼pthread_mutex_trylock將會返回0,並會對該互斥量上鎖。如果互斥量已經被上鎖,那麼會立刻返回EBUSY。

二、條件變量
需要的頭文件:pthread.h
條件變量標識符:pthread_cond_t

1、互斥鎖的存在問題:
互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。設想一種簡單情景:多個線程訪問同一個共享資源時,並不知道何時應該使用共享資源,如果在臨界區里加入判斷語句,或者可以有效,但一來效率不高,二來複雜環境下就難以編寫了,這是我們需要一個結構,能在條件成立時觸發相應線程,進行變量修改和訪問。

2、條件變量:
條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個線程改變了條件變量,它將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變量被用來進行線承間的同步。

pthread_cond_signal用來激活被阻塞並等待在該條件變量cond上的一個線程。存在多個線程阻塞在此條件變量上時,哪一個線程被喚醒是由線程的調度策略喚醒其中的一個。要注意的是,必須用保護條件變量的互斥鎖來保護這個函數,否則條件滿足信號又可能在測試條件和調用pthread_cond_wait函數之間被髮出,從而造成無限制的等待。pthread_cond_broadcast()則激活所有等待線程。

等待條件有兩種方式:無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait()。pthread_cond_wait使線程阻塞在一個條件變量上。線程解開mutex指向的鎖並被條件變量cond阻塞。線程可以被函數pthread_cond_signal和函數 pthread_cond_broadcast喚醒,但是要注意的是,條件變量只是起阻塞和喚醒線程的作用,具體的判斷條件還需用戶給出。線程被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來線程應該仍阻塞在這裏,被等待被下一次喚醒。這個過程一般用while語句實現。計時等待方式如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待。
無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求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()前的加鎖動作對應。

pthread_cond_wait()和pthread_cond_timedwait()都被實現爲取消點,因此,在該處等待的線程將立即重新運行,在重新鎖定mutex後離開 pthread_cond_wait(),然後執行取消動作。也就是說如果pthread_cond_wait()被取消,mutex是保持鎖定狀態的,因而需要定義退出回調函數來爲其解鎖。
pthread_cond_wait實際上可以看作是以下幾個動作的合體:
解鎖線程鎖;
等待條件爲true;
加鎖線程鎖;

使用形式:
// thread a
pthread_mutex_lock(&mutex);
if (condition is true)
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

// thread b
pthread_mutex_lock(&mutex);
while (condition is false)
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
/*線程b中爲什麼使用while呢?因爲在pthread_cond_signal和pthread_cond_wait返回之間,有時間差,假設在這個時間差內,條件改變了,顯然需要重新檢查條件。也就是說在pthread_cond_wait被喚醒的時候可能該條件已經不成立。*/

pthread_cond_destroy()只有在沒有線程在該條件變量上等待的時候才能註銷這個條件變量,否則返回 EBUSY。因爲Linux實現的條件變量沒有分配什麼資源,所以註銷動作只包括檢查是否有等待線程。

三、信號量
信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。每一次調用wait操作將會使semaphore值減1,而如果semaphore值已經爲0,則wait操作將會阻塞。每一次調用post操作將會使semaphore值加1。
需要的頭文件:semaphore.h
信號量標識符:sem_t


信號量與線程鎖、條件變量相比還有以下幾點不同:
1)鎖必須是同一個線程獲取以及釋放,否則會死鎖。而條件變量和信號量則不必。
2)信號的遞增與減少會被系統自動記住,系統內部有一個計數器實現信號量,不必擔心會丟失,而喚醒一個條件變量時,如果沒有相應的線程在等待該條件變量,這次喚醒將被丟失。



[NOTE]
線程數據
  在單線程的程序裏,有兩種基本的數據:全局變量和局部變量。但在多線程程序裏,還有第三種數據類型:線程數據(TSD: Thread-Specific Data)。它和全局變量很象,在線程內部,各個函數可以象使用全局變量一樣調用它,但它對線程外部的其它線程是不可見的。這種數據的必要性是顯而易見的。例如我們常見的變量errno,它返回標準的出錯信息。它顯然不能是一個局部變量,幾乎每個函數都應該可以調用它;但它又不能是一個全局變量,否則在 A線程裏輸出的很可能是B線程的出錯信息。要實現諸如此類的變量,我們就必須使用線程數據。我們爲每個線程數據創建一個鍵,它和這個鍵相關聯,在各個線程裏,都使用這個鍵來指代線程數據,但在不同的線程裏,這個鍵代表的數據是不同的,在同一個線程裏,它代表同樣的數據內容。
  和線程數據pthread_key_t相關的函數主要有4個:創建一個鍵;爲一個鍵指定線程數據;從一個鍵讀取線程數據;刪除鍵。






1 -- 關於pthread條件變量

man pthread_cond_init | col -b > pthread_cond.man得到manual中的描述:

A condition (short for ''condition variable'') is a synchronization device that allows threads to suspend execution and relinquish the processors until some predicate on shared data is satisfied. The basic operations on conditions are: signal the condition(when the predicate becomes true), and wait for the condition, suspending the thread execution until another thread signals the condition.

條件變量是同步線程的一種機制,它允許線程掛起,讓出處理器等待其他線程向它發送信號,該線程收到該信號後被喚醒繼續執行程序。對條件變量基本的操作就是:a)向條件變量發送信號,喚醒等待的線程;b)等待條件變量並掛起直至其他線程向該條件變量發送信號。爲了防止競爭,條件變量總是和一個互斥鎖同時使用。

2 -- 條件變量相關函數

phtread條件變量主要有如下的這些函數:

#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);

返回0爲成功,非0爲失敗。
注意:pthread_cond_init,pthread_cond_signal,pthread_cond_broadcast,pthread_cond_wait只返回0,不會返回其他錯誤碼。也就是說這幾個函數每次調用都會成功,編程時不用檢查返回值。但pthread_cond_timedwait和pthread_cond_destroy會返回錯誤碼,需要注意!

3 -- 條件變量創建

條件變量的初始化有兩種方式:靜態和動態方式。
1.靜態方式
初始化方法:pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;
對於靜態分配的條件變量,如果使用默認的條件變量屬性,可以直接使用PTHREAD_COND_INITIALIZER對條件變量進行賦值來初始化。pthread_cond_t是一個結構體,同樣PTHREAD_COND_INITIALIZER是一個結構體常量。

2.動態方式
動態方式調用pthread_cond_init函數對條件變量初始化,該函數的第二個參數指向條件變量屬性的結構體。儘管POSIX標準中爲條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常設置爲NULL。

4 -- 條件變量等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

pthread_cond_wait調用相當複雜,它是如下執行序列的一個組合:
(1)釋放互斥鎖 並且 線程掛起(這兩個操作是一個原子操作);
(2)線程獲得信號,嘗試獲得互斥鎖後被喚醒;

我們知道調用pthread_cond_signal給條件變量發送信號時,如果當時沒有線程在等待這個條件變量,信號將被丟棄。如果"釋放互斥鎖"和"線程掛起"不是一個原子操作,那麼pthread_cond_wait線程在"釋放互斥鎖"和"線程掛起"之間,如果有信號一旦發生,程序就會錯失一次條件變量變化。

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

返回錯誤碼:ETIMEDOUT。如果在指定時刻前,沒有信號發生。
返回錯誤碼:EINTR。該函數被信號中斷。

pthread_cond_timewait是等待條件變量的令一種形式,與前一種的區別是計時等待方式如果在給定時刻前條件沒有滿足,就返回ETIMEOUT結束等待。該函數在不同情況下會返回特定錯誤碼,編程時請參照開發。

5 -- 條件變量觸發
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal喚醒等待改條件變量所有線程中的一個。如果此時沒有線程在等待該條件變量,那麼就丟棄該信號,當做什麼也沒發生。如果有多個線程在等待,精確保證只有一個線程被喚醒。

int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_broadcast喚醒等待該條件變量所有線程。如果此時沒有線程在等待該條件變量,那麼就丟棄該信號,當做什麼也沒發生。

6 -- 條件變量銷燬
int pthread_cond_destroy(pthread_cond_t *cond);

返回錯誤碼:EBUSY。當前還有線程在該條件變量上等待。

pthread_cond_destroy銷燬一個條件變量,但只有在沒有線程在該條件變量上等待的時候才能銷燬,否則返回EBUSY。因爲Linux實現的條件變量沒有分配什麼資源,所以註銷動作只包括檢查是否有等待線程。

7 -- memcached應用條件變量實例

背景:memcached是一個多線程結構的程序,主線程負責接收和分發請求,工作者線程實際處理請求。工作者線程在主線程中創建。創建線程後,工作者線程需要完成一些初始化工作,才允許主線程繼續執行,所以主線程需要等待這些工作者線程全部初始化完畢。
這裏就使用到了條件變量,大體的流程是這樣的:
(1)主線程初始化一個條件變量和一個互斥鎖;
(2)主線程創建n個工作者線程;
(3)主線程調用pthread_mutex_lock鎖定互斥鎖,然後調用pthread_cond_wait在條件變量上wait,等待被喚醒;
(4)子線程執行初始化代碼,完畢後獲取互斥鎖,累加已初始化線程數量,調用pthread_cond_signal給該條件變量發送信號,同時釋放互斥鎖;
(5)線程調度喚醒主線程,主線程檢查現在已經初始化的線程數目,如果都初始化了就釋放互斥鎖,順序執行其他代碼;如果還沒初始化完畢,調用pthread_cond_wait再次等待。

主線程執行如下代碼:

/*主線程創建nthreads個線程,線程創建後進行初始化,初始化完畢後累加init_count*/
for (i = 0; i < nthreads; i++) 
{
	create_worker(worker_libevent, &threads[i]);
}

/*主線程等待線程全部初始化,條件是已初始化量init_count等於線程數nthreads*/
pthread_mutex_lock(&init_lock);
while (init_count < nthreads) 
{
       	pthread_cond_wait(&init_cond, &init_lock);
}
pthread_mutex_unlock(&init_lock);

工作者線程執行如下代碼:

//TODO:初始化代碼
pthread_mutex_lock(&init_lock);
init_count++;	//累加已初始化線程數量
pthread_cond_signal(&init_cond);
pthread_mutex_unlock(&init_lock);

看完這段代碼之後,估計都會有一個疑問:流程(3)中互斥鎖已被主線程獲取了,在線程全部初始化完畢之前,主線程並沒有顯式釋放互斥鎖,爲什麼在流程(4)中工作者線程還能獲取到互斥鎖呢?在講解pthread_cond_wait函數時說明過這個問題,也可以從下圖看出其中奧妙。

pthread_cond_wait


發佈了28 篇原創文章 · 獲贊 2 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章