【C】——信號量 互斥鎖 條件變量的區別


信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作(大家都在semtake的時候,就阻塞在哪里)。而互斥鎖是用在多線程多任務互斥的,一個線程占用了某一個資源,那么別的線程就無法訪問,直到這個線程unlock,其他的線程才開始可以利用這個資源。比如對全局變量的訪問,有時要加鎖,操作完了,在解鎖。有的時候鎖和信號量會同時使用的”
也就是說,信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以后再進行自己下面的步驟,這個任務並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。而線程互斥量則是“鎖住某一資源”的概念,在鎖定期間內,其他線程無法對被保護的數據進行操作。在有些情況下兩者可以互換。

兩者之間的區別:

作用域
信號量: 進程間或線程間(linux僅線程間)
互斥鎖: 線程間

上鎖時
信號量: 只要信號量的value大於0,其他線程就可以sem_wait成功,成功后信號量的value減一。若value值不大於0,則sem_wait阻塞,直到sem_post釋放后value值加一
互斥鎖: 只要被鎖住,其他任何線程都不可以訪問被保護的資源

成功后否則就阻塞

以下是信號燈(量)的一些概念:

信號燈與互斥鎖和條件變量的主要不同在於”燈”的概念,燈亮則意味着資源可用,燈滅則意味着不可用。如果說后兩中同步方式側重於”等待”操作,即資源不可用的話,信號燈機制則側重於點燈,即告知資源可用;沒有等待線程的解鎖或激發條件都是沒有意義的,而沒有等待燈亮的線程的點燈操作則有效,且能保持燈亮狀態。當然,這樣的操作原語也意味着更多的開銷。

信號燈的應用除了燈亮/燈滅這種二元燈以外,也可以采用大於1的燈數,以表示資源數大於1,這時可以稱之為多元燈。

1. 創建和 注銷

POSIX信號燈標准定義了有名信號燈和無名信號燈兩種,但LinuxThreads的實現僅有無名燈,同時有名燈除了總是可用於多進程之間以外,在使用上與無名燈並沒有很大的區別,因此下面僅就無名燈進行討論。

int sem_init(sem_t *sem, int pshared, unsigned int value)
這是創建信號燈的API,其中value為信號燈的初值,pshared表示是否為多進程共享而不僅僅是用於一個進程。LinuxThreads沒有實現多進程共享信號燈,因此所有非0值的pshared輸入都將使sem_init()返回-1,且置errno為ENOSYS。初始化好的信號燈由sem變量表征,用於以下點燈、滅燈操作。

int sem_destroy(sem_t * sem)
被注銷的信號燈sem要求已沒有線程在等待該信號燈,否則返回-1,且置errno為EBUSY。除此之外,LinuxThreads的信號燈 注銷函數不做其他動作。

2. 點燈和滅燈

int sem_post(sem_t * sem)

點燈操作將信號燈值原子地加1,表示增加一個可訪問的資源。

int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)

sem_wait()為等待燈亮操作,等待燈亮(信號燈值大於0),然后將信號燈原子地減1,並返回。sem_trywait()為sem_wait()的非阻塞版,如果信號燈計數大於0,則原子地減1並返回0,否則立即返回-1,errno置為EAGAIN。

3. 獲取燈值

int sem_getvalue(sem_t * sem, int * sval)

讀取sem中的燈計數,存於*sval中,並返回0。

4. 其他

sem_wait()被實現為取消點,而且在支持原子”比較且交換”指令的體系結構上,sem_post()是唯一能用於異步信號處理函數的POSIX異步信號 安全的API。

----------------------------
線程同步:何時互斥鎖不夠,還需要條件變量?

假設有共享的資源sum,與之相關聯的mutex 是lock_s.假設每個線程對sum的操作很簡單的,與sum的狀態無關,比如只是sum++.那么只用mutex足夠了.程序員只要確保每個線程操作前,取得lock,然后sum++,再unlock即可.每個線程的代碼將像這樣
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
}

  如果操作比較復雜,假設線程t0,t1,t2的操作是sum++,而線程t3則是在sum到達100的時候,打印出一條信息,並對sum清零. 這種情況下,如果只用mutex, 則t3需要一個循環,每個循環里先取得lock_s,然后檢查sum的狀態,如果sum>=100,則打印並清零,然后unlock.如果sum& amp; lt;100,則unlock,並sleep()本線程合適的一段時間.

 這個時候,t0,t1,t2的代碼不變,t3的代碼如下
print()
{
while (1)
{
pthread_mutex_lock(lock_s);
if(sum<100)
{
printf(“sum reach 100!”);
pthread_mutex_unlock(lock_s);
}
else
{
pthread_mutex_unlock(lock_s);
my_thread_sleep(100);
return OK;
}
}
}

這種辦法有兩個問題
1) sum在大多數情況下不會到達100,那么對t3的代碼來說,大多數情況下,走的是else分支,只是lock和unlock,然后sleep().這浪費了CPU處理時間.
2) 為了節省CPU處理時間,t3會在探測到sum沒到達100的時候sleep()一段時間.這樣卻又帶來另外一個問題,亦即t3響應速度下降.可能在sum到達200的時候,t4才會醒過來.
3) 這樣,程序員在設置sleep()時間的時候陷入兩難境地,設置得太短了節省不了資源,太長了又降低響應速度.真是難辦啊!

  這個時候,condition variable內褲外穿,從天而降,拯救了焦頭爛額的你.

  你首先定義一個condition variable.
pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER;

  t0,t1,t2的代碼只要后面加兩行,像這樣
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
if(sum>=100)
pthread_cond_signal(&cond_sum_ready);
}
而t3的代碼則是
print
{
pthread_mutex_lock(lock_s);
while(sum<100)
pthread_cond_wait(&cond_sum_ready, &lock_s);
printf(“sum is over 100!”);
sum=0;
pthread_mutex_unlock(lock_s);
return OK;
}

注意兩點:
1) 在thread_cond_wait()之前,必須先lock相關聯的mutex, 因為假如目標條件未滿足,pthread_cond_wait()實際上會unlock該mutex, 然后block,在目標條件滿足后再重新lock該mutex, 然后返回.
2) 為什么是while(sum<100),而不是if(sum<100) ?這是因為在pthread_cond_signal()和pthread_cond_wait()返回之間,有時間差,假設在這個時間差內,還有另外一個線程t4又把sum減少到100以下了,那么t3在pthread_cond_wait()返回之后,顯然應該再檢查一遍sum的大小.這就是用 while的用意

 

線程間的同步技術,主要以互斥鎖和條件變量為主,條件變量和互斥所的配合使用可以很好的處理對於條件等待的線程間的同步問題。舉個例子:當有兩個變量x,y需要在多線程間同步並且學要根據他們之間的大小比較來啟動不同的線程執行順序,這便用到了條件變量這一技術。看代碼

 1 #include <iostream>  
 2 #include <pthread.h>  
 3 using namespace std;  
 4   
 5     pthread_cond_t qready = PTHREAD_COND_INITIALIZER;    //初始構造條件變量  
 6     pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;    //初始構造鎖  
 7     pthread_t tid1,tid2,tid3;  
 8   
 9     int x = 10;  
10     int y = 20;  
11   
12   
13     void *thrd_1(void *arg)  
14     {  
15         pthread_mutex_lock(&qlock);  
16         while(x<y)  
17         {  
18             pthread_cond_wait(&qready,&qlock);  
19         }  
20         pthread_mutex_unlock(&qlock);  
21         cout<<"1"<<endl;  
22         sleep(5);  
23     }  
24   
25     void *thrd_2(void *arg)  
26     {  
27         pthread_mutex_lock(&qlock);  
28         x = 20;  
29         y = 10;  
30         cout<<"has change x and y"<<endl;  
31   
32         pthread_mutex_unlock(&qlock);  
33         if(x > y)  
34         {  
35             pthread_cond_signal(&qready);  
36         }  
37         cout<<"2"<<endl;  
38     }  
39   
40     void *thrd_3(void *arg)  
41     {  
42         pthread_join(tid1,NULL);  
43         cout<<"3"<<endl;  
44     }  
45   
46     int main(int argc,char **argv)  
47     {  
48         int err;  
49         err = pthread_create(&tid1,NULL,thrd_1,NULL);  
50         if(err != 0)  
51         {  
52             cout<<"pthread 1 create error"<<endl;  
53         }  
54         err = pthread_create(&tid2,NULL,thrd_2,NULL);  
55         if(err != 0)  
56         {  
57             cout<<"pthread 2 create error"<<endl;  
58         }  
59         err = pthread_create(&tid3,NULL,thrd_3,NULL);  
60         if(err != 0)  
61         {  
62             cout<<"pthread 3 create error"<<endl;  
63         }  
64         while(1)  
65         {  
66             sleep(1);  
67         }  
68         return 0;  
69           
70     }

 

可以看到,創建了3個線程后,執行順序2,1,3,即打印出的數字是213。為什么是這個順序呢?我們接下去看,當創建tid1線程的時候,進入線程函數,並且加上了鎖,然后進入pthread_cond_wait函數,這個函數的功能是等待qready這個條件變量成功,這個條件是什么呢?我們稍后在看,現在我們只要知道,這個函數在qready條件沒滿足的時候會卡在這里,並且,會把傳入的互斥鎖解鎖,為什么要解鎖的,擬可以想想,如果不解鎖的話,那外部就沒有可以對x,y的修改權,應為其他兩個線程想要修改這兩個值的話都需要對qclock進行枷鎖。

好了線程1就這樣,那之后就會運行線程2,我們看線程2的線程函數,該函數一開始也加了鎖,但當線程1的pthread_cond_wait解鎖之后,他就可以繼續運行了,並且,在之后,它對x,y進行了修改,改好之后進行了解鎖,並且調用了pthread_cond_signal通知線程1,現在可以知道了吧。這個滿足的條件就是要x>y。
   現在這里有個問題,一定要在發送通知之前解鎖嗎?答案是肯定的,為什么,因為如果先發送通知信號給線程1的時候,pthread_cond_wait可能在線程2的解鎖之前就返回,而當它返回的時候,會再次將這個所進行鎖定,而這個所還沒有在線程2中解鎖,應次會使其在次卡住。雖然這個卡住在線程2運行到解鎖處會消除,但這並不符合我們有時的需求,所以最好還是在解鎖之后在發送信號。(如果看不懂的話,可以參考下面紅色字體的部分!!!)
    所以可以看出為什么線程2總是在線程1之前執行完畢,線程3就更不用說了,pthread_join你們懂的!!!

總結:

  互斥鎖、條件變量、信號量三者的差別:

  (1) 互斥鎖必須總是由給他上鎖的線程解鎖(因為此時其他線程根本得不到此鎖),信號量沒有這種限制:一個線程等待某個信號量,而另一個線程可以掛出該信號量

  (2)每個信號量有一個與之關聯的值,掛出時+1,等待時-1,那么任何線程都可以掛出一個信號,即使沒有線程在等待該信號量的值。不過對於條件變量來說,如果pthread_cond_signal之后沒有任何線程阻塞在pthread_cond_wait上,那么此條件變量上的信號丟失。

  (3)在各種各樣的同步技巧中,能夠從信號處理程序中安全調用的唯一函數是sem_post


免責聲明!

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



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