在上一篇博客中,我們見識了多線程“裸奔”(無同步機制)時導致的銀行賬户錯誤和打印亂碼。為了解決這些問題,我們需要引入一種機制,保證同一時刻只有一個線程能訪問共享資源。
這個機制就是互斥量(Mutex)。你可以把它想象成洗手間門上的鎖:“有人佔用,閒人免進”。
一、 互斥鎖的“使用説明書”
互斥鎖本質上是一個結構體 pthread_mutex_t,但在邏輯上,我們可以把它看作一個初值為 1 的整數:
- 1 (Unlocked):鎖是空閒的,可以進。
- 0 (Locked):鎖被佔用了,必須排隊等待。
核心函數五件套
所有函數成功都返回 0,失敗返回錯誤號。
- 定義鎖:
pthread_mutex_t mutex;(通常定義為全局變量) - 初始化:
pthread_mutex_init(&mutex, NULL); - 加鎖 (阻塞):
pthread_mutex_lock(&mutex);
- 如果鎖可用 (1),減 1 變為 0,線程繼續執行。
- 如果鎖被佔 (0),線程阻塞(掛起睡覺),直到鎖被釋放。
- 解鎖:
pthread_mutex_unlock(&mutex);
- 將鎖的值加 1 變為 1,並喚醒阻塞在該鎖上的線程。
- 銷燬:
pthread_mutex_destroy(&mutex);
二、 代碼實戰:拯救“打印亂碼”
我們重寫之前的“打印機”案例,這次加上互斥鎖,確保 HELLO 和 world 能夠完整輸出,互不打斷。
1. 代碼示例 (mutex_print_fix.c)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
// 1. 定義全局互斥鎖
pthread_mutex_t mutex;
void printer(const char *str) {
// 3. 進入臨界區前加鎖
// 拿到鎖後,其他線程如果也想調 printer,就會卡在這一行等待
pthread_mutex_lock(&mutex);
while (*str != '\0') {
putchar(*str);
fflush(stdout);
usleep(1000); // 即使在這裏放棄CPU,因為持有鎖,其他線程也進不來
str++;
}
printf("\n");
// 4. 離開臨界區後解鎖
pthread_mutex_unlock(&mutex);
}
void *tfn1(void *arg) {
while (1) {
printer("HELLO");
sleep(1); // 模擬其他非共享操作
}
return NULL;
}
void *tfn2(void *arg) {
while (1) {
printer("world");
sleep(1);
}
return NULL;
}
int main() {
pthread_t t1, t2;
// 2. 初始化互斥鎖 (使用默認屬性)
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, tfn1, NULL);
pthread_create(&t2, NULL, tfn2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 5. 銷燬互斥鎖
pthread_mutex_destroy(&mutex);
return 0;
}
2. 運行結果
HELLO
world
HELLO
world
...
效果分析: 再也不會出現 HweorllldO 這種亂碼了。當線程 1 打印 'H' 後即使休眠,線程 2 嘗試進入 printer 函數時會在 pthread_mutex_lock 處被卡住,直到線程 1 打完整個單詞並執行 unlock。
三、 進階技巧:鎖的粒度 (Granularity)
很多初學者覺得:“既然鎖這麼好用,我把整個循環都鎖起來不就更安全了嗎?” 這是一個大誤區!
1. 鎖的粒度對比
錯誤做法(粗粒度):
void *tfn(void *arg) {
while (1) {
pthread_mutex_lock(&mutex); // 加鎖
printer("HELLO");
sleep(1); // 把 sleep 也鎖住了!
pthread_mutex_unlock(&mutex); // 解鎖
}
}
- 後果:線程拿着鎖去睡覺(
sleep)。在它睡覺的 1 秒鐘裏,鎖被佔用,其他線程完全無法工作。這導致多線程程序變成了“串行程序”,併發性極差。
正確做法(細粒度):
void *tfn(void *arg) {
while (1) {
// 僅保護共享資源操作(打印)
pthread_mutex_lock(&mutex);
printer("HELLO");
pthread_mutex_unlock(&mutex);
// 耗時的私有操作放在鎖外面
sleep(1);
}
}
- 原則:鎖的粒度越小越好。只在訪問共享數據的那一瞬間加鎖,用完立刻解開。
四、 特殊操作:非阻塞加鎖 (trylock)
有時我們不希望線程死等一個鎖,如果鎖被佔用了,不如先去幹點別的事。這時可以使用 pthread_mutex_trylock。
1. 代碼示例 (mutex_trylock.c)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <errno.h> // 需要包含錯誤號頭文件
pthread_mutex_t mutex;
void *tfn(void *arg) {
printf("--- [線程] 嘗試去拿鎖...\n");
int ret = pthread_mutex_trylock(&mutex);
if (ret == 0) {
printf("--- [線程] 拿到鎖了!執行任務...\n");
sleep(2); // 模擬工作
pthread_mutex_unlock(&mutex);
printf("--- [線程] 任務完成,解鎖\n");
} else if (ret == EBUSY) {
printf("--- [線程] 鎖被別人佔着 (EBUSY),我不等了,去玩會遊戲\n");
} else {
printf("--- [線程] 出錯了: %s\n", strerror(ret));
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
// 主線程先拿走鎖
printf(">>> [主線程] 先把鎖拿走\n");
pthread_mutex_lock(&mutex);
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
sleep(1); // 讓子線程運行
printf(">>> [主線程] 釋放鎖\n");
pthread_mutex_unlock(&mutex);
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
2. 運行結果
>>> [主線程] 先把鎖拿走
--- [線程] 嘗試去拿鎖...
--- [線程] 鎖被別人佔着 (EBUSY),我不等了,去玩會遊戲
>>> [主線程] 釋放鎖
分析:子線程發現鎖被主線程佔用,沒有阻塞等待,而是直接返回 EBUSY 錯誤,執行了 else if 分支。
五、 知識總結
|
知識點
|
核心內容
|
避坑指南
|
|
互斥鎖本質 |
初值為 1 的整數,0 代表鎖定
|
是一種建議鎖,防君子不防小人(如果不加鎖直接訪問也能訪問,但會亂)。 |
|
加鎖 |
阻塞等待,直到拿到鎖
|
注意成對使用,否則死鎖。
|
|
嘗試加鎖 |
非阻塞,失敗返回 |
適用於不需要死等的場景。
|
|
鎖粒度 |
鎖的範圍大小
|
越小越好。嚴禁把 |
|
注意事項 |
全局變量定義
|
局部變量會導致不同線程看到的不是同一把鎖。
|