條件變量:等待與信號發送
使用互斥鎖雖然可以解決一些資源競爭的問題,但互斥鎖只有兩種狀態(加鎖和解鎖),這限制了互斥鎖的用途。
條件變量(條件鎖)也可以解決線程同步和共享資源訪問的問題,條件變量是對互斥鎖的補充,它允許一個線程阻塞並等待另一個線程發送的信號,當收到信號時,阻塞的線程被喚醒並試圖鎖定與之相關的互斥鎖。
條件變量初始化
條件變量和互斥鎖一樣,都有靜態動態兩種創建方式,靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
動態方式調用函數int pthread_cond_init,API定義如下:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
條件變量的屬性由參數attr指定,如果參數attr為NULL,那么就使用默認的屬性設置。盡管POSIX標准中為條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常為NULL,且被忽略。多線程不能同時初始化一個條件變量,因為這是原子操作。如果函數調用成功,則返回0,並將新創建的條件變量的ID放在參數cond中。
解除條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
調用
destroy
函數解除條件變量並不會釋放存儲條件變量的內存空間。
條件變量阻塞(等待)
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 *abtime);
等待有兩種方式:條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式如果在給定時刻前條件沒有滿足,則返回ETIMEDOUT,結束等待,其中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()前的加鎖動作對應。阻塞時處於解鎖狀態。
激活
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal
函數的作用是發送一個信號給另外一個正在處於阻塞等待狀態的線程
,
使其脫離阻塞狀態
,
繼續執行,如果沒有線程處在阻塞等待狀態
,pthread_cond_signal
也會成功返回。
共享變量的狀態改變必須遵守
lock/unlock
的規則:需要在同一互斥鎖的保護下使用
pthread_cond_signal
(即
pthread_cond_wait
必須放在
pthread_mutex_lock
和
pthread_mutex_unlock
之間)否則條件變量可以在對關聯條件變量的測試和
pthread_cond_wait
帶來的阻塞之間獲得信號,這將導致無限期的等待(死鎖)。因為他要根據共享變量的狀態來決定是否要等待,所以為了避免死鎖,必須要在
lock/unlock
隊中。
共享變量的狀態改變必須遵守
lock/unlock
的規則:
pthread_cond_signal
即可以放在
pthread_mutex_lock
和
pthread_mutex_unlock
之間,也可以放在
pthread_mutex_lock
和
pthread_mutex_unlock
之后,但是各有優缺點。
若為前者,在某些線程的實現中,會造成等待線程從內核中喚醒(由於
cond_signal)
然后又回到內核空間(因為
cond_wait
返回后會有原子加鎖的行為),所以一來一回會有性能的問題(上下文切換)。詳細來說就是,當一個等待線程被喚醒的時候,它必須首先加鎖互斥量(參見
pthread_cond_wait()
執行步驟)。如果線程被喚醒而此時通知線程任然鎖住互斥量,則被喚醒線程會立刻阻塞在互斥量上,等待通知線程解鎖該互斥量,引起線程的上下文切換。當通知線程解鎖后,被喚醒線程繼續獲得鎖,再一次的引起上下文切換。這樣導致被喚醒線程不能順利加鎖,延長了加鎖時間,加重了系統不必要的負擔。但是在
LinuxThreads
或者
NPTL
里面,就不會有這個問題,因為在
Linux
線程中,有兩個隊列,分別是
cond_wait
隊列和
mutex_lock
隊列,
cond_signal
只是讓線程從
cond_wait
隊列移到
mutex_lock
隊列,而不用返回到用戶空間,不會有性能的損耗,因此
Linux
推薦這種形式。
而后者不會出現之前說的那個潛在的性能損耗,因為在
signal
之前就已經釋放鎖了。但如果
unlock
和
signal
之前,有個低優先級的線程正在
mutex
上等待的話,那么這個低優先級的線程就會搶占高優先級的線程(
cond_wait
的線程
)
。而且,假設而這在上面的放中間的模式下是不會出現的。
而對於
pthread_cond_broadcast
函數,它使所有由參數
cond
指向的條件變量阻塞的線程退出阻塞狀態,如果沒有阻塞線程,則函數無效。
實例代碼如下
:
void *produce_cond(void *arg)
{
for(;;)
{
pthread_mutex_lock(&put.mutex);
if(put.nput >= nitems)
{
pthread_mutex_unlock(&put.mutex);
return NULL;
}
buff[put.nput]=put.nval;
put.nput++;
put.nval++;
pthread_mutex_unlock(&put.mutex);
pthread_mutex_lock(&nready.mutex);
if(nready.nready == 0)
printf("produce_cond nready==0\n");
pthread_cond_signal(&nready.cond);
nready.nready++;
pthread_mutex_unlock(&nready.mutex);
*((int *)arg)+=1;
}
}
void *consume_cond(void *arg)
{
int i;
for(i=0;i<3;i++)
{
pthread_mutex_lock(&nready.mutex);
printf("consume_cond nready =%d\n",&nready);
while(nready.nready == 0)
pthread_cond_wait(&nready.cond,&nready.mutex);
nready.nready--;
pthread_mutex_unlock(&nready.mutex);
printf("consume_cond\n");
if(buff[i]!=i)
{
printf("buff[%d]=%d\n",i,buff[i]);
}
}
}
在
produce_cond
中給用來統計准備好由消費者處理的條目數的計數器
nready.nready
加一。在加
1
之前,如果該計數器的值為
0
,就調用
pthread_cond_signal
喚醒可能正在等待其值變為非零的任意消費者線程。該計數器是在生產者和消費者之間共享的。因此只有鎖住與之關聯的互斥所
nready.mutex
時才能訪問它。
consume_cond
中消費者只是等待計數器
nready.nready
變為非零,既然該計數器是在所有生產者和消費者之間共享的。那么只有鎖住與之關聯的互斥鎖時才能測試它的值。如果在鎖住該互斥鎖期間該計數器的值為
0
,我們就調用
pthread_cond_wait
進入睡眠,該函數原子地執行以下兩個動作:
1
給互斥鎖
nread.mutex
解鎖
2
把調用線程投入睡眠,直到另外某個線程就本條件變量調用
pthread_cond_signal.
pthread_cond_wait
在返回前重新給互斥鎖
nready.mutex
上鎖。因此當它返回並且我們發現計數器
nready.nready
不為
0
時,我們就該把計數器減一,然后給該互斥鎖解鎖