互斥鎖與條件變數學習與應用小結

Rice_rice發表於2024-06-01

互斥鎖,也叫互斥量。有以下幾個顯著的特點:

  1. 唯一性:互斥鎖保證在任何給定的時間點,只有一個執行緒可以獲得對臨界區資源的訪問權。如果一個執行緒鎖定了一個互斥量,在它解除鎖定之前,沒有其他執行緒可以鎖定這個互斥量。

  2. 原子性:鎖定和解鎖互斥鎖的操作是原子的,這意味著作業系統(或pthread函式庫)保證瞭如果一個執行緒鎖定了一個互斥量,沒有其他執行緒在同一時間可以成功鎖定這個互斥量。

  3. 非繁忙等待:當一個執行緒已經鎖定了一個互斥量,其他試圖鎖定這個互斥量的執行緒將被掛起(不佔用任何CPU資源),直到第一個執行緒解除對這個互斥量的鎖定為止。被掛起的執行緒在鎖被釋放後會被喚醒並繼續執行。

  4. 保護共享資源:互斥鎖用於保護臨界區資源免受多個執行緒同時訪問和修改的影響,確保資料的完整性和一致性。

    ​ 另外,互斥鎖,也成為“協同鎖”或“建議鎖”。當 A執行緒對某個全域性變數加鎖訪問,8在訪問前嘗試加鎖,拿不到鎖,8阻塞。C執行緒不去加鎖,而直接訪問該全域性變數,依然能夠訪問,但會出現資料混亂。

    ​ 雖然它提供了鎖定機制來避免多執行緒同時訪問共享資源造成的競態條件,但並沒有強制限定執行緒必須遵循這一機制。也就是說,即使有互斥鎖存在,如果執行緒不按照規則來訪問資料,依然可能造成資料混亂。也就是說,互斥鎖的有效性依賴於程式設計者的合作。因此,程式設計時需要根據程式設計人員的規則使用

條件變數不是鎖,必須與互斥鎖一起配合使用,因此在這同時記錄兩者的API介面及用法。

互斥鎖使用時,有幾個技巧小結如下:

  1. 儘量保證鎖的粒度,越小越好。(即訪問共享資料前,加鎖,訪問結束要立即解鎖,讓其他執行緒能訪問到的機率更大,保證較高的併發度
  2. 將互斥鎖變數mutex看成整數1,每次上鎖即申請資源,mutex--;解鎖即釋放資源,mutex++,類似於訊號量。

常用函式列舉如下:

pthread_mutex_t mutex (= PTHREAD_MUTEX_INITIALIZER);                                // 互斥鎖變數初始定義,本質是一個結構體,應用時可忽略
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *mutexattr); // 初始化(若已經用宏定義進行初始化,則不需要呼叫此函式),第二個引數一般用NULL。
//此處的restrict是關鍵字,表示對於 mutex指標指向的空間操作,均只能由mutex指標完成,不能依靠傳遞地址靠其他變數完成,換句話說,就是告訴編譯器不會有其他指標指向同一塊記憶體,從而允許編譯器進行更高效的最佳化。
int pthread_mutex_lock(pthread_mutex_t *mutex);                                       // 上鎖
int pthread_mutex_trylock(pthread_mutex_t *mutex);                                       // 嘗試進行上鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);                                    // 解鎖
int pthread_mutex_destroy(pthread_cond_t *cond);                                      // 銷燬

條件變數的特點羅列如下:

  1. 等待與通知機制:條件變數允許執行緒在某個特定條件不滿足時進入等待狀態(等待在條件變數上)。當其他執行緒改變了條件,並認為等待的執行緒應該被喚醒時,它會使用條件變數的通知(signal)或廣播(broadcast)功能來喚醒等待的執行緒。
  2. 與互斥鎖結合使用:條件變數必須與互斥鎖一起使用,以確保在檢查和修改條件時的原子性。在呼叫條件變數的等待函式時,鎖定互斥鎖,然後檢查條件。如果條件不滿足,則呼叫條件變數的等待函式並釋放互斥鎖,進入等待狀態。當條件變數被通知後,執行緒會重新獲取互斥鎖並繼續執行。
  3. 避免忙等待:使用條件變數可以避免執行緒在條件不滿足時持續檢查條件(即忙等待),這樣可以節省CPU資源。執行緒在等待條件變數時會被掛起,直到被其他執行緒通知。
  4. 廣播與通知:條件變數通常提供通知(notify)和廣播(notifyAll)功能。通知只會喚醒等待在條件變數上的一個執行緒,而廣播會喚醒所有等待在條件變數上的執行緒。

常用函式列舉如下:

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); // 等待一段時間(abstime),
int pthread_cond_destroy(pthread_cond_t *cond);                                                           // 銷燬

應用舉例如下:

/**
 * @file name:	互斥鎖與條件變數的應用展示
 * @brief	 :  子執行緒功能詳note
 * @author ni456xinmie@163.com
 * @date 2024/06/01
 * @version 1.0 :版本
 * @property :
 * @note
 *          子執行緒B:當全域性變數x和y滿足條件時(>100),進行列印輸出
 *          子執行緒C:對共享資源x和y進行每次+50的操作
 *          子執行緒D:對共享資源x和y進行每次+30的操作,並調整sleep函式與unlock函式的位置,與子執行緒C進行對比
 * CopyRight (c)  2023-2024   ni456xinmie@163.com   All Right Reseverd
 */

#include <pthread.h> //關於執行緒API介面的標頭檔案   編譯時需要指定  -pthread
#include <stdio.h>
#include <unistd.h>
int x = 0, y = 0; // 共享資源

pthread_cond_t cond_flag = PTHREAD_COND_INITIALIZER; // 此處已用宏定義,就不用初始化函式進行初始化了
pthread_mutex_t mutex_flag = PTHREAD_MUTEX_INITIALIZER;

void *task_B(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 上鎖,“申請資源”
        while (x < 100 && y < 100)
        {
            pthread_cond_wait(&cond_flag, &mutex_flag); // 條件阻塞,即當x,y<100時,進行掛起等待其他執行緒的訊號
        }
        printf("x 和y已達到要求:%d %d\n", x, y);
        x = 0;
        y = 0;
        pthread_mutex_unlock(&mutex_flag); // 解鎖,“釋放資源”
        sleep(2);                          // 為保證輸出效果進行延時
    }
}

void *task_C(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 上鎖,“申請資源”
        x += 50;
        y += 50;						 //申請到資源後對x和y進行操作
        printf("子執行緒C對x和y進行操作:%d %d\n", x, y);
        if (x > 100 && y > 100)
        {
            pthread_cond_signal(&cond_flag);
        }								   //滿足條件後,傳送一條訊號給子執行緒B
        pthread_mutex_unlock(&mutex_flag); // 解鎖,“釋放資源”
        sleep(2);                          // 為方便展示,進行延遲
    }
}

void *task_D(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 同子執行緒C
        x += 30;
        y += 30;
        printf("子執行緒C對x和y進行操作:%d %d\n", x, y);
        if (x > 100 && y > 100)
        {
            pthread_cond_signal(&cond_flag);
        }
        sleep(2);                          // 此處與子執行緒C進行對比,先sleep,再解鎖
        pthread_mutex_unlock(&mutex_flag); 
    }
}
int main(int argc, char const *argv[])
{
    // 1.建立子執行緒
    pthread_t B_tid;
    pthread_create(&B_tid, NULL, task_B, NULL); // 子執行緒B
    pthread_t C_tid;
    pthread_create(&C_tid, NULL, task_C, NULL); // 子執行緒C
    pthread_t D_tid;
    pthread_create(&D_tid, NULL, task_D, NULL); // 子執行緒C

    // 2.主執行緒結束
    pthread_exit(NULL);
    return 0; // 主執行緒在上一條語句已經結束,這條語句永遠不會執行
}

上述程式僅執行子執行緒B與C時,輸出結果同預期,如下:

子執行緒C對x和y進行操作:50 50
子執行緒C對x和y進行操作:100 100
子執行緒C對x和y進行操作:150 150
x 和y已達到要求:150 150
子執行緒C對x和y進行操作:50 50
子執行緒C對x和y進行操作:100 100
子執行緒C對x和y進行操作:150 150
x 和y已達到要求:150 150
子執行緒C對x和y進行操作:50 50
子執行緒C對x和y進行操作:100 100
子執行緒C對x和y進行操作:150 150
x 和y已達到要求:150 150

上述程式執行子執行緒B,C與D時,輸出結果如下:

子執行緒C對x和y進行操作:50 50
子執行緒C對x和y進行操作:80 80
子執行緒C對x和y進行操作:110 110
子執行緒C對x和y進行操作:140 140
子執行緒C對x和y進行操作:170 170
子執行緒C對x和y進行操作:200 200
子執行緒C對x和y進行操作:230 230
子執行緒C對x和y進行操作:260 260
子執行緒C對x和y進行操作:290 290
子執行緒C對x和y進行操作:320 320
子執行緒C對x和y進行操作:350 350
x 和y已達到要求:350 350
子執行緒C對x和y進行操作:30 30
子執行緒C對x和y進行操作:60 60
子執行緒C對x和y進行操作:90 90
子執行緒C對x和y進行操作:120 120
子執行緒C對x和y進行操作:150 150

​ 可見,子執行緒D搶佔資源頻繁,子執行緒C一直在等待資源,而子執行緒B滿足條件收到訊號以後,也無法搶到共享資源進行輸出,併發度不高。因此,為了提高粒度,需要子執行緒D在對x和y進行操作後立即進行解鎖,然後sleep阻塞,讓其他執行緒有機會得到共享資源,提高併發度。

相關文章