在上一篇博客中,我們見識了多線程“裸奔”(無同步機制)時導致的銀行賬户錯誤和打印亂碼。為了解決這些問題,我們需要引入一種機制,保證同一時刻只有一個線程能訪問共享資源。

這個機制就是互斥量(Mutex)。你可以把它想象成洗手間門上的鎖:“有人佔用,閒人免進”


一、 互斥鎖的“使用説明書”

互斥鎖本質上是一個結構體 pthread_mutex_t,但在邏輯上,我們可以把它看作一個初值為 1 的整數:

  • 1 (Unlocked):鎖是空閒的,可以進。
  • 0 (Locked):鎖被佔用了,必須排隊等待。

核心函數五件套

所有函數成功都返回 0,失敗返回錯誤號。

  1. 定義鎖pthread_mutex_t mutex; (通常定義為全局變量)
  2. 初始化pthread_mutex_init(&mutex, NULL);
  3. 加鎖 (阻塞)pthread_mutex_lock(&mutex);
  • 如果鎖可用 (1),減 1 變為 0,線程繼續執行。
  • 如果鎖被佔 (0),線程阻塞(掛起睡覺),直到鎖被釋放。
  1. 解鎖pthread_mutex_unlock(&mutex);
  • 將鎖的值加 1 變為 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 代表鎖定

是一種建議鎖,防君子不防小人(如果不加鎖直接訪問也能訪問,但會亂)。

加鎖 lock

阻塞等待,直到拿到鎖

注意成對使用,否則死鎖。

嘗試加鎖 trylock

非阻塞,失敗返回 EBUSY

適用於不需要死等的場景。

鎖粒度

鎖的範圍大小

越小越好。嚴禁把 sleep 等耗時操作放在鎖裏。

注意事項

全局變量定義

局部變量會導致不同線程看到的不是同一把鎖。

Linux 多線程編程:互斥鎖 (Mutex) —— 給共享資源加上“安全鎖”_#include