多線程的互斥鎖和條件變量

在Linux下使用C語言編寫多線程程序時(shí),條件變量和互斥鎖是常用的同步機(jī)制,用于確保線程之間的正確協(xié)作。以下是它們的基本用法和理解:

一、互斥鎖和條件變量的基本用法和理解

1.1 互斥鎖(Mutex):

  • 互斥鎖用于確保在任何給定時(shí)刻只有一個(gè)線程可以訪問被保護(hù)的共享資源,以防止競態(tài)條件(Race Condition)。
  • 互斥鎖有兩個(gè)主要操作:鎖定(Lock)和解鎖(Unlock)。
  • 鎖定互斥鎖后,其他線程會(huì)被阻塞,直到擁有鎖的線程釋放它。
  • 在C語言中,你可以使用pthread_mutex_init初始化互斥鎖,pthread_mutex_lock鎖定它,pthread_mutex_unlock解鎖它,最后使用pthread_mutex_destroy銷毀它。

1.2 條件變量(Condition Variable):

  • 條件變量用于線程之間的通信和協(xié)作,允許線程等待某個(gè)特定條件的發(fā)生。
  • 條件變量通常與互斥鎖一起使用,以確保線程在檢查條件和等待條件滿足之間的操作是原子的。
  • 條件變量有兩個(gè)主要操作:等待(Wait)和通知(Signal)。
  • 等待操作使線程等待某個(gè)條件的滿足,并且會(huì)在條件滿足或者被其他線程發(fā)出的通知后繼續(xù)執(zhí)行。
  • 通知操作用于通知等待的線程條件已滿足。

1.3 總結(jié)

理解互斥鎖和條件變量的關(guān)鍵點(diǎn)在于:

  • 互斥鎖用于保護(hù)共享資源,確保一次只有一個(gè)線程能夠修改或訪問它。

  • 條件變量用于線程之間的協(xié)作,使一個(gè)線程能夠等待另一個(gè)線程發(fā)出的信號(hào),以便在滿足某些條件時(shí)繼續(xù)執(zhí)行。

二、經(jīng)典示例

多線程經(jīng)典的示例是生產(chǎn)者-消費(fèi)者問題,其中生產(chǎn)者線程生產(chǎn)數(shù)據(jù),消費(fèi)者線程消費(fèi)數(shù)據(jù),它們必須同步以避免競態(tài)條件。

其中,互斥鎖用于保護(hù)共享緩沖區(qū),條件變量用于通知消費(fèi)者何時(shí)可以消費(fèi)數(shù)據(jù)。

生產(chǎn)者-消費(fèi)者示例代碼如下:

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

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0; // 記錄緩沖區(qū)中的數(shù)據(jù)數(shù)量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;

void *producer(void *arg) {
    int item = 1;
    while (1) {
        pthread_mutex_lock(&mutex); // 鎖定互斥鎖
        while (count == BUFFER_SIZE) { // 如果緩沖區(qū)已滿,等待
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[count] = item; // 將數(shù)據(jù)放入緩沖區(qū)
        count++;
        printf("Produced: %d\n", item);
        pthread_cond_signal(&not_empty); // 通知消費(fèi)者緩沖區(qū)不為空
        pthread_mutex_unlock(&mutex); // 解鎖互斥鎖
        item++;
        sleep(1); // 模擬生產(chǎn)時(shí)間
    }
}

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[count - 1];
        count--;
        printf("Consumed: %d\n", item);
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
        sleep(2); // 模擬消費(fèi)時(shí)間
    }
}

int main() {
    pthread_t producer_thread, consumer_thread;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);
    
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    
    return 0;
}

這個(gè)示例演示了一個(gè)簡單的生產(chǎn)者-消費(fèi)者問題,其中有一個(gè)生產(chǎn)者線程和一個(gè)消費(fèi)者線程,它們共享一個(gè)有限大小的緩沖區(qū)(buffer)?;コ怄i mutex 用于保護(hù)緩沖區(qū)的訪問,條件變量not_fullnot_empty用于通知生產(chǎn)者何時(shí)可以繼續(xù)生產(chǎn)和消費(fèi)者何時(shí)可以繼續(xù)消費(fèi)。

生產(chǎn)者線程生成數(shù)據(jù)并將其放入緩沖區(qū),消費(fèi)者線程從緩沖區(qū)中取出數(shù)據(jù)并處理。它們使用條件變量來等待合適的時(shí)機(jī)來執(zhí)行這些操作,以避免競態(tài)條件。

此示例中的sleep函數(shù)用于模擬生產(chǎn)和消費(fèi)的時(shí)間,以便更容易觀察線程的交互。

三、代碼詳解

3.1 頭文件

首先,包括必要的頭文件和定義了一些全局變量

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

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0; // 記錄緩沖區(qū)中的數(shù)據(jù)數(shù)量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;
  • buffer 是一個(gè)大小為5的整數(shù)數(shù)組,用于表示共享的緩沖區(qū)
  • count 用于跟蹤緩沖區(qū)中的數(shù)據(jù)數(shù)量
  • pthread_mutex_tpthread_cond_t 分別是互斥鎖和條件變量的類型

3.2 生產(chǎn)者代碼

接著,producer 函數(shù)是生產(chǎn)者線程的入口函數(shù)。它使用互斥鎖來確保在訪問共享資源(buffercount)時(shí)沒有競爭。

void *producer(void *arg) {
    int item = 1;
    while (1) {
        pthread_mutex_lock(&mutex); // 鎖定互斥鎖
        while (count == BUFFER_SIZE) { // 如果緩沖區(qū)已滿,等待
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[count] = item; // 將數(shù)據(jù)放入緩沖區(qū)
        count++;
        printf("Produced: %d\n", item);
        pthread_cond_signal(&not_empty); // 通知消費(fèi)者緩沖區(qū)不為空
        pthread_mutex_unlock(&mutex); // 解鎖互斥鎖
        item++;
        sleep(1); // 模擬生產(chǎn)時(shí)間
    }
}

具體來說:

  • pthread_mutex_lock(&mutex) 鎖定互斥鎖,防止其他線程同時(shí)進(jìn)入臨界區(qū)。
  • while (count == BUFFER_SIZE) 檢查緩沖區(qū)是否已滿,如果滿了就調(diào)用 pthread_cond_wait 進(jìn)入等待狀態(tài),等待消費(fèi)者線程通知它緩沖區(qū)不再滿。
  • 一旦條件允許,生產(chǎn)者將數(shù)據(jù)放入緩沖區(qū),增加 count 計(jì)數(shù),并打印出生產(chǎn)的數(shù)據(jù)。
  • pthread_cond_signal(&not_empty) 通知消費(fèi)者線程緩沖區(qū)不再為空,可以繼續(xù)消費(fèi)。
  • 最后,解鎖互斥鎖 pthread_mutex_unlock(&mutex),以允許其他線程進(jìn)入臨界區(qū)。

3.3 消費(fèi)者代碼

然后,consumer 函數(shù)是消費(fèi)者線程的入口函數(shù),它執(zhí)行類似的操作:

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[count - 1];
        count--;
        printf("Consumed: %d\n", item);
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
        sleep(2); // 模擬消費(fèi)時(shí)間
    }
}
  • 先鎖定互斥鎖,確保在訪問共享資源之前沒有競爭。
  • 然后檢查緩沖區(qū)是否為空,如果是,就等待條件變量 not_empty 的通知。
  • 一旦條件滿足,消費(fèi)者從緩沖區(qū)中取出數(shù)據(jù),減少 count 計(jì)數(shù),并打印出消費(fèi)的數(shù)據(jù)。
  • 最后,通過 pthread_cond_signal(&not_full) 通知生產(chǎn)者線程緩沖區(qū)不再滿,可以繼續(xù)生產(chǎn)。

3.4 main函數(shù)

最后,main 函數(shù)是程序的入口點(diǎn),它初始化互斥鎖和條件變量,然后創(chuàng)建了生產(chǎn)者和消費(fèi)者線程。通過 pthread_join 等待線程的結(jié)束,并在程序結(jié)束前銷毀互斥鎖和條件變量。

int main() {
    pthread_t producer_thread, consumer_thread;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);
    
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    
    return 0;
}

3.5 總結(jié)

這個(gè)示例演示了如何使用互斥鎖和條件變量來實(shí)現(xiàn)線程之間的協(xié)作,確保生產(chǎn)者和消費(fèi)者線程可以正確地共享和操作緩沖區(qū),同時(shí)避免競態(tài)條件。這是一個(gè)基本的多線程同步示例,用于理解互斥鎖和條件變量的基本概念。在實(shí)際應(yīng)用中,可能需要更復(fù)雜的同步和錯(cuò)誤處理機(jī)制。

四、Q&A

4.1 為什么一個(gè)線程上鎖了,另一個(gè)線程就無法訪問,另一個(gè)線程怎么知道上鎖了?

在程序中人為規(guī)定代碼,線程只有成功獲取鎖,才可以訪問共享資源!

例如以下模板就是獲取鎖后才進(jìn)行操作:

// 在線程中嘗試獲取鎖
if (pthread_mutex_lock(&mutex) == 0) {
    // 成功獲取鎖,可以訪問共享資源
    // ...
    // 釋放鎖
    pthread_mutex_unlock(&mutex);
} else {
    // 鎖已經(jīng)被其他線程占用,無法訪問共享資源
    // 在此處可以選擇等待或執(zhí)行其他操作
}

或者

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex); //等待獲取鎖,才可以訪問共享資源
        ...
        pthread_mutex_unlock(&mutex); //對(duì)共享資源操作完,釋放鎖
        ... //執(zhí)行自己的邏輯
    }
}

在多線程的線程代碼中,執(zhí)行前首先判斷是否能獲取鎖,獲取鎖后才能繼續(xù)執(zhí)行程序,否則等待。

注意:多線程的應(yīng)用中通常會(huì)有多個(gè)鎖,每個(gè)鎖用于不同的目的或保護(hù)不同的共享資源,線程需要獲取自己需要的鎖,鎖的數(shù)量和種類取決于應(yīng)用的復(fù)雜性和需要。

舉一個(gè)例子,考慮一個(gè)簡單的文件系統(tǒng)模擬,其中有多個(gè)文件對(duì)象,每個(gè)文件對(duì)象都需要保護(hù)以防止并發(fā)訪問。在這種情況下,可以使用多個(gè)鎖,每個(gè)鎖用于保護(hù)一個(gè)文件對(duì)象。這樣,不同的線程可以并行訪問不同的文件對(duì)象,而不會(huì)產(chǎn)生爭用條件。

當(dāng)一個(gè)線程獲取了互斥鎖(Mutex)并鎖定了共享資源時(shí),其他線程嘗試獲取相同互斥鎖會(huì)被阻塞。這是因?yàn)榛コ怄i的主要作用是確保在任何給定時(shí)刻只有一個(gè)線程可以訪問被保護(hù)的共享資源,以防止多個(gè)線程同時(shí)修改它,從而避免競態(tài)條件(Race Condition)。

另一個(gè)線程如何知道互斥鎖已經(jīng)被上鎖了?這是因?yàn)榫€程在嘗試獲取鎖時(shí),如果鎖已經(jīng)被其他線程占用,那么獲取鎖的操作會(huì)被阻塞,直到互斥鎖被釋放。在這種情況下,線程會(huì)等待在獲取鎖的地方,無法繼續(xù)執(zhí)行。這種等待是線程被動(dòng)感知鎖的狀態(tài)的方式。

要知道是否成功獲取互斥鎖,通常線程可以檢查獲取鎖的函數(shù)的返回值。在C語言中,使用pthread_mutex_lock來嘗試獲取鎖,它會(huì)返回0表示成功獲取鎖,如果鎖已被其他線程占用,則會(huì)阻塞等待,并在成功獲取鎖后返回。

4.2 pthread_cond_wait(&not_empty, &mutex);這里為什么還要用mutex變量這個(gè)函數(shù)干什么?

pthread_cond_wait 函數(shù)用于在等待條件變量的同時(shí),釋放互斥鎖。這個(gè)函數(shù)通常與互斥鎖一起使用,以實(shí)現(xiàn)復(fù)雜的線程同步。

當(dāng)一個(gè)線程調(diào)用 pthread_cond_wait 時(shí),它會(huì)進(jìn)入等待狀態(tài),并且會(huì)釋放與傳遞給函數(shù)的互斥鎖相關(guān)聯(lián)的互斥鎖。這是因?yàn)榈却龡l件變量的線程期望在某個(gè)條件滿足時(shí)被喚醒,而在等待期間允許其他線程訪問互斥鎖保護(hù)的共享資源。

具體到代碼行 pthread_cond_wait(&not_empty, &mutex);

  • &not_empty 是條件變量,它用于等待某個(gè)條件的發(fā)生。在這個(gè)示例中,它用于等待緩沖區(qū)不為空。
  • &mutex 是互斥鎖,它用于保護(hù)共享資源(這里是 buffercount)。在等待條件變量期間,互斥鎖會(huì)被釋放,以允許其他線程訪問共享資源。

當(dāng)其他線程修改了共享資源并且認(rèn)為某個(gè)條件已滿足時(shí),它們可以調(diào)用 pthread_cond_signalpthread_cond_broadcast 來通知等待的線程條件已經(jīng)滿足,從而喚醒等待的線程。一旦被喚醒,等待的線程會(huì)嘗試重新獲取互斥鎖,以繼續(xù)執(zhí)行,并檢查條件是否真的滿足。

所以,pthread_cond_wait 的作用是等待條件變量的信號(hào),同時(shí)釋放互斥鎖以允許其他線程訪問共享資源,從而實(shí)現(xiàn)線程之間的協(xié)作和同步。

4.3 sleep的時(shí)間

這里的sleep時(shí)間模仿的是實(shí)際代碼邏輯處理時(shí)間,如進(jìn)行協(xié)議解析等。

sleep時(shí)間的不同將導(dǎo)致程序處于不同的狀態(tài),在生產(chǎn)者-消費(fèi)者問題示例中,sleep 的時(shí)間設(shè)置將影響緩沖區(qū)的狀態(tài)線程之間的交互方式。

具體來說,不同的 sleep 時(shí)間設(shè)置可以影響以下方面:

1. 緩沖區(qū)狀態(tài)

  • 如果生產(chǎn)者和消費(fèi)者的 sleep 時(shí)間相對(duì)較短,緩沖區(qū)可能很快填滿或清空,取決于生產(chǎn)速度和消費(fèi)速度。

  • 如果 sleep 時(shí)間較長,可能會(huì)導(dǎo)致緩沖區(qū)保持在某種狀態(tài),例如,生產(chǎn)者和消費(fèi)者都處于等待狀態(tài),或者緩沖區(qū)在某個(gè)特定數(shù)量的數(shù)據(jù)上保持平衡。

2. 線程交互:

  • 線程的 sleep 時(shí)間設(shè)置會(huì)影響線程之間的交互方式。較短的 sleep 時(shí)間可能導(dǎo)致線程頻繁地爭奪互斥鎖,從而增加競爭,但也可能導(dǎo)致較高的上下文切換。

  • 較長的 sleep 時(shí)間可能減少線程爭奪鎖的頻率,但也可能導(dǎo)致線程在等待期間浪費(fèi)時(shí)間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容