詳解Linux多線程中互斥鎖、讀寫鎖、自旋鎖、條件變量、信號量

linux兵工廠 發佈 2024-04-30T13:41:10.573948+00:00

Hello、Hello大家好,我是木榮,今天我們繼續來聊一聊Linux中多線程編程中的重要知識點,詳細談談多線程中同步和互斥機制。同步和互斥互斥:多線程中互斥是指多個線程訪問同一資源時同時只允許一個線程對其進行訪問,具有唯一性和排它性。

Hello、Hello大家好,我是木榮,今天我們繼續來聊一聊linux中多線程編程中的重要知識點,詳細談談多線程中同步和互斥機制。

同步和互斥

  • 互斥:多線程中互斥是指多個線程訪問同一資源時同時只允許一個線程對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的;
  • 同步:多線程同步是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。

互斥鎖

在多任務作業系統中,同時運行的多個任務可能都需要使用同一種資源。為了同一時刻只允許一個任務訪問資源,需要用互斥鎖對資源進行保護。互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態,即上鎖( lock )和解鎖( unlock )。

互斥鎖操作基本流程

  1. 訪問共享資源前,對互斥鎖進行加鎖
  2. 完成加鎖後訪問共享資源
  3. 對共享資源完成訪問後,對互斥鎖進行解鎖

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

互斥鎖特性

  • 原子性:互斥鎖是一個原子操作,作業系統保證如果一個線程鎖定了一個互斥鎖,那麼其他線程在同一時間不會成功鎖定這個互斥鎖
  • 唯一性:如果一個線程鎖定了一個互斥鎖,在它解除鎖之前,其他線程不可以鎖定這個互斥鎖
  • 非忙等待:如果一個線程已經鎖定了一個互斥鎖,第二個線程又試圖去鎖定這個互斥鎖,則第二個線程將被掛起且不占用任何CPU資源,直到第一個線程解除對這個互斥鎖的鎖定為止,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥鎖

示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

 char *pTestBuf = nullptr; // 全局變量

 /* 定義互斥鎖 */
pthread_mutex_t mutex;

void *ThrTestMutex(void *p)
{
    pthread_mutex_lock(&mutex);     // 加鎖
    {
        pTestBuf = (char*)p;
        sleep(1);
    }
    pThread_mutex_unlock(&mutex);   // 解鎖
}

int main()
{   
    /* 初始化互斥量, 默認屬性 */
    pthread_mutex_init(&mutex, NULL);

    /* 創建兩個線程對共享資源訪問 */
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThrTestMutex, (void *)"Thread1");
    pthread_create(&tid2, NULL, ThrTestMutex, (void *)"Thread2"); 

    /* 等待線程結束 */
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL); 

    /* 銷毀互斥鎖 */
    pthread_mutex_destroy(&mutex);  

    return 0;
}

讀寫鎖

  • 讀寫鎖允許更高的並行性,也叫共享互斥鎖。互斥量要麼是加鎖狀態,要麼就是解鎖狀態,而且一次只有一個線程可以對其加鎖。讀寫鎖可以有3種狀態:讀模式下加鎖狀態、寫模式加鎖狀態、不加鎖狀態。 一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占有讀模式的讀寫鎖,即允許多個線程讀但只允許一個線程寫。
  • 當讀操作較多,寫操作較少時,可用讀寫鎖提高線程讀並發性

讀寫鎖特性

  1. 如果有線程讀數據,則允許其它線程執行讀操作,但不允許寫操作
  2. 如果有線程寫數據,則其它線程都不允許讀、寫操作
  3. 如果某線程申請了讀鎖,其它線程可以再申請讀鎖,但不能申請寫鎖
  4. 如果某線程申請了寫鎖,其它線程不能申請讀鎖,也不能申請寫鎖
  5. 讀寫鎖適合於對數據的讀次數比寫次數多得多的情況

讀寫鎖創建和銷毀

    #include <pthread.h>
    int phtread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 參數:rwlock:讀寫鎖,attr:讀寫鎖屬性
  • 返回值:成功返回0,出錯返回錯誤碼

讀寫鎖加鎖解鎖

    #include <pthread.h>
    /** 加讀鎖 */
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    /** 加寫鎖 */
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    /** 釋放鎖 */
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 參數:rwlock:讀寫鎖
  • 返回值:成功返回 0;出錯,返回錯誤碼

示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

/* 定義讀寫鎖 */
pthread_rwlock_t rwlock;

/* 定義共享資源變量 */
int g_nNum = 0;

/* 讀操作 其他線程允許讀操作 不允許寫操作 */
void *fun1(void *arg)  
{  
    while(1)  
    {  
        pthread_rwlock_rdlock(&rwlock);  
        {
            printf("read thread 1 == %d\n", g_nNum);
        }      
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
}  

/* 讀操作,其他線程允許讀操作,不允許寫操作 */
void *fun2(void *arg)
{    
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);  
        {
            printf("read thread 2 == %d\n", g_nNum);
        }      
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
} 

/* 寫操作,其它線程都不允許讀或寫操作 */
void *fun3(void *arg)
{    
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);
        {
            g_nNum++;        
            printf("write thread 1\n");
        }
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
} 
/* 寫操作,其它線程都不允許讀或寫操作 */ 
void *fun4(void *arg)
{    
    while(1)
    {  
        pthread_rwlock_wrlock(&rwlock);  
        {
            g_nNum++;  
            printf("write thread 2\n");  
        }
        pthread_rwlock_unlock(&rwlock); 

        sleep(1);  
    }  
}  
  
int main(int arc, char *argv[])  
{  
    pthread_t ThrId1, ThrId2, ThrId3, ThrId4;  
      
    pthread_rwlock_init(&rwlock, NULL);  // 初始化一個讀寫鎖  
      
    /* 創建測試線程 */
    pthread_create(&ThrId1, NULL, fun1, NULL);  
    pthread_create(&ThrId2, NULL, fun2, NULL);  
    pthread_create(&ThrId3, NULL, fun3, NULL);  
    pthread_create(&ThrId4, NULL, fun4, NULL);  
      
    /* 等待線程結束,回收其資源 */
    pthread_join(ThrId1, NULL);  
    pthread_join(ThrId2, NULL);  
    pthread_join(ThrId3, NULL);  
    pthread_join(ThrId4, NULL);  
      
    pthread_rwlock_destroy(&rwlock);      // 銷毀讀寫鎖  
      
    return 0;  
}
  • 結果

自旋鎖

  • 自旋鎖與互斥鎖功能相同,唯一不同的就是互斥鎖阻塞後休眠不占用CPU,而自旋鎖阻塞後不會讓出CPU,會一直忙等待,直到得到鎖
  • 自旋鎖在用戶態較少用,而在內核態使用的比較多
  • 自旋鎖的使用場景:鎖的持有時間比較短,或者說小於2次上下文切換的時間
  • 自旋鎖在用戶態的函數接口和互斥量一樣,把pthread_mutex_lock()/pthread_mutex_unlock()中mutex換成spin,如:pthread_spin_init()

自旋鎖函數

  • linux中的自旋鎖用結構體spinlock_t 表示,定義在include/linux/spinlock_type.h。自旋鎖的接口函數全部定義在include/linux/spinlock.h頭文件中,實際使用時只需include<linux/spinlock.h>即可

示例

    include<linux/spinlock.h>
    spinlock_t lock;      //定義自旋鎖
    spin_lock_init(&lock);   //初始化自旋鎖
    spin_lock(&lock);      //獲得鎖,如果沒獲得成功則一直等待
    {
        .......         //處理臨界資源
    }
    spin_unlock(&lock);     //釋放自旋鎖

條件變量

  • 條件變量用來阻塞一個線程,直到條件發生。通常條件變量和互斥鎖同時使用。條件變量使線程可以睡眠等待某種條件滿足。條件變量是利用線程間共享的全局變量進行同步的一種機制。
  • 條件變量的邏輯: 一個線程掛起去等待條件變量的條件成立,而另一個線程使條件成立。

基本原理

線程在改變條件狀態之前先鎖住互斥量。如果條件為假,線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發信號給關聯的條件變量,喚醒一個或多個等待它的線程。如果兩進程共享可讀寫的內存,條件變量可以被用來實現這兩進程間的線程同步

示例

#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 *ThrFun1(void *name)  
{  
    char *p = (char *)name;  
    
    // 加鎖,把信號量加入隊列,釋放信號量
    pthread_mutex_lock(&taximutex); 
    {
        pthread_cond_wait(&taxicond, &taximutex);  
    } 
    pthread_mutex_unlock(&taximutex);  

    printf ("ThrFun1: %s now got a signal!\n", p);  
    pthread_exit(NULL);  
}  
  
void *ThrFun2(void *name)  
{  
    char *p = (char *)name;  
    printf ("ThrFun2: %s cond signal.\n", p);    // 發信號
    pthread_cond_signal(&taxicond);  
    pthread_exit(NULL);  
}  
  
int main (int argc, char **argv)  
{  
    pthread_t Thread1, Thread2;  
    pthread_attr_t threadattr;
    pthread_attr_init(&threadattr);  // 線程屬性初始化
  
    // 創建三個線程 
    pthread_create(&Thread1, &threadattr, ThrFun1, (void *)"Thread1");  
    sleep(1);  

    pthread_create(&Thread2, &threadattr, ThrFun2, (void *)"Thread2");  
    sleep(1);   

    pthread_join(Thread1, NULL);
    pthread_join(Thread2, NULL);
  
    return 0;  
}
  • 結果

虛假喚醒

  • 當線程從等待已發出信號的條件變量中醒來,卻發現它等待的條件不滿足時,就會發生虛假喚醒。之所以稱為虛假,是因為該線程似乎無緣無故地被喚醒了。但是虛假喚醒不會無緣無故發生:它們通常是因為在發出條件變量信號和等待線程最終運行之間,另一個線程運行並更改了條件

避免虛假喚醒

  • 在wait端,我們必須把判斷條件和wait()放到while循環中
    pthread_mutex_lock(&taximutex); 
    {
        while(value != wantValue)
        {
            pthread_cond_wait(&taxicond, &taximutex);  
        }
    } 
    pthread_mutex_unlock(&taximutex); 

信號量

  • 信號量用於進程或線程間的同步和互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。編程時可根據操作信號量值的結果判斷是否對公共資源具有訪問的權限,當信號量值大於0時,則可以訪問,否則將阻塞
#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 s8Test = 'a'; 

void *pthread_g(void *arg)  //此線程改變字符的值
{    
    while(1)
    {
        sem_wait(&sem_g);
        s8Test++;
        sleep(2);
        sem_post(&sem_p);
    }
} 
void *pthread_p(void *arg)  //此線程列印字符的值
{    
    while(1)
    {
        sem_wait(&sem_p);        
        printf("%c",s8Test);
        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;
}
  • 結果
關鍵字: