互斥鎖、條件變量、讀寫鎖、自旋鎖、信號量

一、互斥鎖(同步)

  在多任務操作系統中,同時運行的多個任務可能都需要使用同一種資源。這個過程有點類似於,公司部門裏,我在使用着打印機打印東西的同時(還沒有打印完),別人剛好也在此刻使用打印機打印東西,如果不做任何處理的話,打印出來的東西肯定是錯亂的。

  在線程裏也有這麼一把鎖——互斥鎖(mutex),互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態, 即上鎖 (lock) 和解鎖( unlock )。

【互斥鎖的特點】:

  1. 原子性:把一個互斥量鎖定爲一個原子操作,這意味着操作系統(或 pthread 函數庫)保證瞭如果一個線程鎖定了一個互斥量,沒有其他線程在同一時間可以成功鎖定這個互斥量;

  2. 唯一性:如果一個線程鎖定了一個互斥量,在它解除鎖定之前,沒有其他線程可以鎖定這個互斥量;

  3. 非繁忙等待:如果一個線程已經鎖定了一個互斥量,第二個線程又試圖去鎖定這個互斥量,則第二個線程將被掛起(不佔用任何 cpu 資源),直到第一個線程解除對這個互斥量的鎖定爲止,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量。

【互斥鎖的操作流程如下】:

  1. 在訪問共享資源後臨界區域前,對互斥鎖進行加鎖;

  2. 在訪問完成後釋放互斥鎖導上的鎖。在訪問完成後釋放互斥鎖導上的鎖;

  3. 對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。

#include <pthread.h>#include <time.h>// 初始化一個互斥鎖。int pthread_mutex_init(pthread_mutex_t *mutex, 
						const pthread_mutexattr_t *attr);// 對互斥鎖上鎖,若互斥鎖已經上鎖,則調用者一直阻塞,// 直到互斥鎖解鎖後再上鎖。int pthread_mutex_lock(pthread_mutex_t *mutex);// 調用該函數時,若互斥鎖未加鎖,則上鎖,返回 0;// 若互斥鎖已加鎖,則函數直接返回失敗,即 EBUSY。int pthread_mutex_trylock(pthread_mutex_t *mutex);// 當線程試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock 互斥量// 原語允許綁定線程阻塞時間。即非阻塞加鎖互斥量。int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);// 對指定的互斥鎖解鎖。int pthread_mutex_unlock(pthread_mutex_t *mutex);// 銷燬指定的一個互斥鎖。互斥鎖在使用完畢後,// 必須要對互斥鎖進行銷燬,以釋放資源。int pthread_mutex_destroy(pthread_mutex_t *mutex);

【Demo】(阻塞模式):

//使用互斥量解決多線程搶佔資源的問題#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>#include <string.h>
 char* buf[5]; //字符指針數組  全局變量int pos; //用於指定上面數組的下標
 //1.定義互斥量pthread_mutex_t mutex; 
void *task(void *p){    //3.使用互斥量進行加鎖
    pthread_mutex_lock(&mutex);
 
    buf[pos] = (char *)p;
    sleep(1);
    pos++; 
    //4.使用互斥量進行解鎖
    pthread_mutex_unlock(&mutex);
} 
int main(void){    //2.初始化互斥量, 默認屬性
    pthread_mutex_init(&mutex, NULL); 
    //1.啓動一個線程 向數組中存儲內容
    pthread_t tid, tid2;
    pthread_create(&tid, NULL, task, (void *)"zhangfei");
    pthread_create(&tid2, NULL, task, (void *)"guanyu");    //2.主線程進程等待,並且打印最終的結果
    pthread_join(tid, NULL);
    pthread_join(tid2, NULL); 
    //5.銷燬互斥量
    pthread_mutex_destroy(&mutex); 
    int i = 0;    printf("字符指針數組中的內容是:");    for(i = 0; i < pos; ++i)
    {        printf("%s ", buf[i]);
    }    printf("\n");    return 0;
}

【Demo】(非阻塞模式):

#include <stdio.h>#include <pthread.h>#include <time.h>#include <string.h>
 int main (void){    int err;    struct timespec tout;
    struct tm *tmp;
    char buf[64];    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    
    pthread_mutex_lock (&lock);    printf ("mutex is locked\n");
    clock_gettime (CLOCK_REALTIME, &tout);
    tmp = localtime (&tout.tv_sec); 
    strftime (buf, sizeof (buf), "%r", tmp);    printf ("current time is %s\n", buf);
    tout.tv_sec += 10;
    err = pthread_mutex_timedlock (&lock, &tout);
    clock_gettime (CLOCK_REALTIME, &tout);
    tmp = localtime (&tout.tv_sec);
    strftime (buf, sizeof (buf), "%r", tmp);    printf ("the time is now %s\n", buf);    if (err == 0)        printf ("mutex locked again\n");    else 
        printf ("can`t lock mutex again:%s\n", strerror (err));    return 0;
}

二、條件變量(同步)

  與互斥鎖不同,條件變量是用來等待而不是用來上鎖的。條件變量用來自動阻塞一個線程,直 到某特殊情況發生爲止。通常條件變量和互斥鎖同時使用。

  條件變量使我們可以睡眠等待某種條件出現。條件變量是利用線程間共享的全局變量進行同步 的一種機制,主要包括兩個動作:

一個線程等待 "條件變量的條件成立" 而掛起;

另一個線程使 “條件成立”(給出條件成立信號)。

【原理】:

  條件的檢測是在互斥鎖的保護下進行的。線程在改變條件狀態之前必須首先鎖住互斥量。如果一個條件爲假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發信號給關聯的條件變量,喚醒一個或多個等待它的線程,重新獲得互斥鎖,重新評價條件。如果兩進程共享可讀寫的內存,條件變量 可以被用來實現這兩進程間的線程同步。

【條件變量的操作流程如下】:

  1. 初始化:init() 或者 pthread_cond_tcond=PTHREAD_COND_INITIALIER;屬性置爲 NULL;

  2. 等待條件成立:pthread_wait,pthread_timewait.wait() 釋放鎖, 並阻塞等待條件變量爲真 timewait() 設置等待時間, 仍未 signal, 返回 ETIMEOUT(加鎖保證只有一個線程 wait);

  3. 激活條件變量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待線程)

  4. 清除條件變量:destroy; 無線程等待, 否則返回 EBUSY 清除條件變量: destroy; 無線程等待, 否則返回 EBUSY

#include <pthread.h>// 初始化條件變量int pthread_cond_init(pthread_cond_t *cond,						pthread_condattr_t *cond_attr);// 阻塞等待int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);// 超時等待int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,						const timespec *abstime);// 解除所有線程的阻塞int pthread_cond_destroy(pthread_cond_t *cond);// 至少喚醒一個等待該條件的線程int pthread_cond_signal(pthread_cond_t *cond);// 喚醒等待該條件的所有線程int pthread_cond_broadcast(pthread_cond_t *cond);

1、線程的條件變量實例 1

  Jack 開着一輛出租車來到一個站點停車,看見沒人就走了。過段時間,Susan 來到站點準備乘車,但是沒有來,於是就等着。過了一會 Mike 開着車來到了這個站點,Sunsan 就上了 Mike 的車走了。如圖所示:

#include <stdio.h>  #include <stdlib.h>  #include <unistd.h>  #include <pthread.h>  
  pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER;  
pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER;  
  
void *traveler_arrive(void *name)  {  
    char *p = (char *)name;  
  
    printf ("Travelr: %s need a taxi now!\n", p);  
    // 加鎖,把信號量加入隊列,釋放信號量
    pthread_mutex_lock(&taximutex);  
    pthread_cond_wait(&taxicond, &taximutex);  
    pthread_mutex_unlock(&taximutex);  
    printf ("traveler: %s now got a taxi!\n", p);  
    pthread_exit(NULL);  
}  
  
void *taxi_arrive(void *name)  {  
    char *p = (char *)name;  
    printf ("Taxi: %s arrives.\n", p);    // 給線程或者條件發信號,一定要在改變條件狀態後再給線程發信號
    pthread_cond_signal(&taxicond);  
    pthread_exit(NULL);  
}  
  
int main (int argc, char **argv)  {  
    char *name;  
    pthread_t thread;  
    pthread_attr_t threadattr; // 線程屬性 
    pthread_attr_init(&threadattr);  // 線程屬性初始化
  
    // 創建三個線程
    name = "Jack";  
    pthread_create(&thread, &threadattr, taxi_arrive, (void *)name);  
    sleep(1);  
    name = "Susan";  
    pthread_create(&thread, &threadattr, traveler_arrive, (void *)name);  
    sleep(1);  
    name = "Mike";  
    pthread_create(&thread, &threadattr, taxi_arrive, (void *)name);  
    sleep(1);  
  
    return 0;  
}

2、線程的條件變量實例 2

  Jack 開着一輛出租車來到一個站點停車,看見沒人就等着。過段時間,Susan 來到站點準備乘車看見了 Jack 的出租車,於是就上去了。過了一會 Mike 開着車來到了這個站點,看見沒人救等着。如圖所示:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>
 int travelercount = 0;pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER;pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; 
void *traveler_arrive(void *name){    char *p = (char *)name;
 
    pthread_mutex_lock(&taximutex); 
    printf ("traveler: %s need a taxi now!\n", p);
    travelercount++;
    pthread_cond_wait(&taxicond, &taximutex);
            
    pthread_mutex_unlock(&taximutex);    printf ("traveler: %s now got a taxi!\n", p);
    pthread_exit(NULL);
} 
void *taxi_arrive(void *name){    char *p = (char *)name;    printf ("Taxi: %s arrives.\n", p);    for(;;)
    {        if(travelercount)
        {
            pthread_cond_signal(&taxicond);
            travelercount--;            break;
        }
    }
    pthread_exit(NULL);
} 
int main (int argc, char **argv){    char *name;    pthread_t thread;    pthread_attr_t threadattr;
    pthread_attr_init(&threadattr);
 
    name = "Jack";
    pthread_create(&thread, &threadattr, taxi_arrive, name);
    sleep(1);
    name = "Susan";
    pthread_create(&thread, &threadattr, traveler_arrive, name);
    sleep(3);
    name = "Mike";
    pthread_create(&thread, &threadattr, taxi_arrive, name);
    sleep(4); 
    return 0;
}

3、虛假喚醒 (spurious wakeup)

  虛假喚醒 (spurious wakeup) 在採用條件等待時:

while(條件不滿足)
{  
   condition_wait(cond, mutex);  
}  
// 而不是:  If( 條件不滿足 )
{  
   Condition_wait(cond,mutex);  
}

這是因爲可能會存在虛假喚醒”spurious wakeup” 的情況。

  也就是說,即使沒有線程調用 condition_signal, 原先調用 condition_wait 的函數也可能會返回。此時線程被喚醒了,但是條件並不滿足,這個時候如果不對條件進行檢查而往下執行,就可能會導致後續的處理出現錯誤。

  虛假喚醒在 linux 的多處理器系統中 / 在程序接收到信號時可能回發生。在 Windows 系統和 JAVA 虛擬機上也存在。在系統設計時應該可以避免虛假喚醒,但是這會影響條件變量的執行效率,而既然通過 while 循環就能避免虛假喚醒造成的錯誤,因此程序的邏輯就變成了 while 循環的情況。

四、讀寫鎖(同步)

  讀寫鎖與互斥量類似,不過讀寫鎖允許更改的並行性,也叫共享互斥鎖。互斥量要麼是鎖住狀態,要麼就是不加鎖狀態,而且一次只有一個線程可以對其加鎖。讀寫鎖可以有 3 種狀態:讀模式下加鎖狀態、寫模式加鎖狀態、不加鎖狀態。

  一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖(允許多個線程讀但只允許一個線程寫)。

【讀寫鎖的特點】:

如果有其它線程讀數據,則允許其它線程執行讀操作,但不允許寫操作;

如果有其它線程寫數據,則其它線程都不允許讀、寫操作。

【讀寫鎖的規則】:

如果某線程申請了讀鎖,其它線程可以再申請讀鎖,但不能申請寫鎖;

如果某線程申請了寫鎖,其它線程不能申請讀鎖,也不能申請寫鎖。

讀寫鎖適合於對數據結構的讀次數比寫次數多得多的情況。

#include <pthread.h>// 初始化讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *rwlock, 
						const pthread_rwlockattr_t *attr)
						const pthread_rwlockattr_t *attr); 
// 申請讀鎖int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 
// 申請寫鎖int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 
// 嘗試以非阻塞的方式來在讀寫鎖上獲取寫鎖,// 如果有任何的讀者或寫者持有該鎖,則立即失敗返回。int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 
// 解鎖int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 
// 銷燬讀寫鎖int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

【Demo】:

// 一個使用讀寫鎖來實現 4 個線程讀寫一段數據是實例。// 在此示例程序中,共創建了 4 個線程,// 其中兩個線程用來寫入數據,兩個線程用來讀取數據#include <stdio.h>  #include <unistd.h>  
#include <pthread.h>  
pthread_rwlock_t rwlock; //讀寫鎖  int num = 1;  
  
//讀操作,其他線程允許讀操作,卻不允許寫操作  void *fun1(void *arg)  {  
    while(1)  
    {  
        pthread_rwlock_rdlock(&rwlock);        printf("read num first == %d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}  
//讀操作,其他線程允許讀操作,卻不允許寫操作  void *fun2(void *arg){    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);        printf("read num second == %d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
} 
//寫操作,其它線程都不允許讀或寫操作  void *fun3(void *arg){    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);
        num++;        printf("write thread first\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
} 
//寫操作,其它線程都不允許讀或寫操作  void *fun4(void *arg){    while(1)
    {  
        pthread_rwlock_wrlock(&rwlock);  
        num++;  
        printf("write thread second\n");  
        pthread_rwlock_unlock(&rwlock);  
        sleep(1);  
    }  
}  
  
int main()  {  
    pthread_t ptd1, ptd2, ptd3, ptd4;  
      
    pthread_rwlock_init(&rwlock, NULL);//初始化一個讀寫鎖  
      
    //創建線程  
    pthread_create(&ptd1, NULL, fun1, NULL);  
    pthread_create(&ptd2, NULL, fun2, NULL);  
    pthread_create(&ptd3, NULL, fun3, NULL);  
    pthread_create(&ptd4, NULL, fun4, NULL);  
      
    //等待線程結束,回收其資源  
    pthread_join(ptd1, NULL);  
    pthread_join(ptd2, NULL);  
    pthread_join(ptd3, NULL);  
    pthread_join(ptd4, NULL);  
      
    pthread_rwlock_destroy(&rwlock);//銷燬讀寫鎖  
      
    return 0;  
}

五、自旋鎖(同步)

  自旋鎖與互斥量功能一樣,唯一一點不同的就是互斥量阻塞後休眠讓出 cpu,而自旋鎖阻塞後不會讓出 cpu,會一直忙等待,直到得到鎖。

  自旋鎖在用戶態使用的比較少,在內核使用的比較多!自旋鎖的使用場景:鎖的持有時間比較短,或者說小於 2 次上下文切換的時間。

  自旋鎖在用戶態的函數接口和互斥量一樣,把 pthread_mutex_xxx() 中 mutex 換成 spin,如:pthread_spin_init()。

六、信號量(同步與互斥)

  信號量廣泛用於進程或線程間的同步和互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。

  編程時可根據操作信號量值的結果判斷是否對公共資源具有訪問的權限,當信號量值大於 0 時,則可以訪問,否則將阻塞。PV 原語是對信號量的操作,一次 P 操作使信號量減1,一次 V 操作使信號量加1。

#include <semaphore.h>// 初始化信號量int sem_init(sem_t *sem, int pshared, unsigned int value);// 信號量 P 操作(減 1)int sem_wait(sem_t *sem);// 以非阻塞的方式來對信號量進行減 1 操作int sem_trywait(sem_t *sem);// 信號量 V 操作(加 1)int sem_post(sem_t *sem);// 獲取信號量的值int sem_getvalue(sem_t *sem, int *sval);// 銷燬信號量int sem_destroy(sem_t *sem);

【信號量用於同步】:

// 信號量用於同步實例#include <stdio.h>#include <unistd.h>#include <pthread.h>#include <semaphore.h>
 sem_t sem_g,sem_p;   //定義兩個信號量char ch = 'a'; 
void *pthread_g(void *arg)  //此線程改變字符ch的值{    while(1)
    {
        sem_wait(&sem_g);
        ch++;
        sleep(1);
        sem_post(&sem_p);
    }
} 
void *pthread_p(void *arg)  //此線程打印ch的值{    while(1)
    {
        sem_wait(&sem_p);        printf("%c",ch);
        fflush(stdout);
        sem_post(&sem_g);
    }
} 
int main(int argc, char *argv[]){    pthread_t tid1,tid2;
    sem_init(&sem_g, 0, 0); // 初始化信號量爲0
    sem_init(&sem_p, 0, 1); // 初始化信號量爲1
    
    // 創建兩個線程
    pthread_create(&tid1, NULL, pthread_g, NULL);
    pthread_create(&tid2, NULL, pthread_p, NULL);    
    // 回收線程
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);    
    return 0;
}

【信號量用於互斥】:

// 信號量用於互斥實例#include <stdio.h>#include <pthread.h>#include <unistd.h>#include <semaphore.h>
 sem_t sem; //信號量
 void printer(char *str){
    sem_wait(&sem);//減一,p操作
    while(*str) // 輸出字符串(如果不用互斥,此處可能會被其他線程入侵)
    {        putchar(*str);  
        fflush(stdout);
        str++;
        sleep(1);
    }    printf("\n");
    
    sem_post(&sem);//加一,v操作} 
void *thread_fun1(void *arg){    char *str1 = "hello";
    printer(str1);
} 
void *thread_fun2(void *arg){    char *str2 = "world";
    printer(str2);
} 
int main(void){    pthread_t tid1, tid2;
    
    sem_init(&sem, 0, 1); //初始化信號量,初始值爲 1
    
    //創建 2 個線程
    pthread_create(&tid1, NULL, thread_fun1, NULL);
    pthread_create(&tid2, NULL, thread_fun2, NULL);    
    //等待線程結束,回收其資源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL); 
    
    sem_destroy(&sem); //銷燬信號量
    
    return 0;
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dXjiSkyQtiejQ8GjO_f-7g