在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(¬_full, &mutex);
}
buffer[count] = item; // 將數(shù)據(jù)放入緩沖區(qū)
count++;
printf("Produced: %d\n", item);
pthread_cond_signal(¬_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(¬_empty, &mutex);
}
int item = buffer[count - 1];
count--;
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_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(¬_full, NULL);
pthread_cond_init(¬_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(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
這個(gè)示例演示了一個(gè)簡單的生產(chǎn)者-消費(fèi)者問題,其中有一個(gè)生產(chǎn)者線程和一個(gè)消費(fèi)者線程,它們共享一個(gè)有限大小的緩沖區(qū)(buffer)?;コ怄i mutex 用于保護(hù)緩沖區(qū)的訪問,條件變量not_full和not_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_t和pthread_cond_t分別是互斥鎖和條件變量的類型
3.2 生產(chǎn)者代碼
接著,producer 函數(shù)是生產(chǎn)者線程的入口函數(shù)。它使用互斥鎖來確保在訪問共享資源(buffer 和 count)時(shí)沒有競爭。
void *producer(void *arg) {
int item = 1;
while (1) {
pthread_mutex_lock(&mutex); // 鎖定互斥鎖
while (count == BUFFER_SIZE) { // 如果緩沖區(qū)已滿,等待
pthread_cond_wait(¬_full, &mutex);
}
buffer[count] = item; // 將數(shù)據(jù)放入緩沖區(qū)
count++;
printf("Produced: %d\n", item);
pthread_cond_signal(¬_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(¬_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(¬_empty, &mutex);
}
int item = buffer[count - 1];
count--;
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_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(¬_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(¬_full, NULL);
pthread_cond_init(¬_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(¬_full);
pthread_cond_destroy(¬_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(¬_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(¬_empty, &mutex);:
-
¬_empty是條件變量,它用于等待某個(gè)條件的發(fā)生。在這個(gè)示例中,它用于等待緩沖區(qū)不為空。 -
&mutex是互斥鎖,它用于保護(hù)共享資源(這里是buffer和count)。在等待條件變量期間,互斥鎖會(huì)被釋放,以允許其他線程訪問共享資源。
當(dāng)其他線程修改了共享資源并且認(rèn)為某個(gè)條件已滿足時(shí),它們可以調(diào)用 pthread_cond_signal 或 pthread_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í)間。