Linux - 線程互斥

互斥量(mutex)

很多變量需要在線程間共享,這個變量就稱爲共享變量,可以通過共享數據完成線程之間的交互.
但是,多個線程併發的操作共享變量就會出現問題.

如下模擬實現一個網上購票系統:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100; //表示當前的票有多少張

void* BuyTicket(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket, %d\n",s,ticket);
            --ticket;
        }
        else
        {
            break;
        }
    }
    return NULL;
}

int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
    pthread_create(&t2,NULL,BuyTicket,(void*)"thread 2");
    pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
    pthread_create(&t4,NULL,BuyTicket,(void*)"thread 4");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);
    return 0;
}

演示出現錯誤的結果:


出現以上情況的原因:
  • if語句判斷爲真以後,代碼可以併發的切換到其他的線程
  • usleep這個是模擬漫長的業務過程,在這個業務過程中,就可能有多個線程進入該代碼段
  • --ticket本就不是一個原子操作


要解決上面的問題,需要做到以下三點:
  • 代碼必須要有互斥行爲: 當代碼進入臨界區執行的時候,不允許其他線程進入該臨界區
  • 如果多個線程的代碼同時要求執行臨界區的代碼,並且臨界區沒有線程在執行,那麼只允許一個線程進入臨界區
  • 如果線程不在臨界區執行,那麼該線程不能阻止其他線程進入臨界區

所以就此引入我們的互斥量

互斥量接口

初始化互斥量
  • 靜態分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 動態分配
int pthread_mutex_init(pyhread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);

參數:
    mutex: 要初始化的互斥量
    attr:填爲NULL即可

銷燬互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:
  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷燬
  • 不要銷燬一個已經加鎖的互斥量
  • 已經銷燬的互斥量,要確保後面不會有線程再嘗試加鎖

互斥量的加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 

返回值:
    成功返回0,失敗返回錯誤號

調用 pthread_lock 時可能會出現以下情況:
  • 互斥量處於未加鎖狀態,那麼就會對這個互斥量加鎖,同時返回成功
  • 如果這個互斥量已經加鎖,這是 pthread_lock 就會進入阻塞狀態,等待互斥量解鎖

根據互斥量改進上面的購票系統:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t g_lock; //創建一個互斥量

int ticket = 100; //表示當前的票有多少張

void* BuyTicket(void* arg)
{
    char* s = (char*)arg;
    while(1)
    {
        pthread_mutex_lock(&g_lock); //加鎖
        if(ticket > 0)
        {
            usleep(100);
            printf("%s buy ticket, %d\n",s,ticket);
            --ticket;
            pthread_mutex_unlock(&g_lock); //解鎖
        }
        else
        {
            pthread_mutex_unlock(&g_lock); //解鎖
            break;
        }
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&g_lock,NULL); //初始化互斥量

    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
    pthread_create(&t2,NULL,BuyTicket,(void*)"thread 2");
    pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
    pthread_create(&t4,NULL,BuyTicket,(void*)"thread 4");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);

    pthread_mutex_destroy(&g_lock); //銷燬互斥量
    return 0;
}

死鎖

如果在上面的代碼中,加了鎖以後沒有解鎖,會出現什麼問題呢?

比內存泄露更可怕的事情!!! 死鎖!!! 
出現內存泄露, 對於一個256G內存的服務器來說, 往往需要很久纔會將內存消耗殆盡. 而且企業中的服務 器往往會定期進行 "例行重啓".
出現死鎖, 系統中的某些線程會直接停止工作, 導致整個服務器的功能瞬間失效!! 

死鎖的兩個常見場景: 
  • 一個線程獲取到鎖之後, 又嘗試獲取鎖, 就會出現死鎖.
  • 兩個線程A和B. 線程A獲取了鎖1, 線程B獲取了鎖2. 然後A嘗試獲取鎖2, B嘗試獲取鎖1. 這個時候雙方都 無法拿到對方的鎖. 並且會在獲取鎖的函數中阻塞等待. 如果線程數和鎖的數目更多了, 就會使死鎖問題更容易出現, 問題場景更復雜. 對於這種需要獲取多個鎖的場景, 規定所有的線程都按照固定的順序來獲取鎖, 能夠一定程度上避免死鎖

總結成一句話就是,拿了鎖卻沒有及時釋放,就會產生死鎖

線程安全和可重入

可重入函數: 在多個執行流中被同時調用不會存在邏輯上的問題. 
線程安全函數: 在多線程被同時調用不會存在問題. 

這兩個概念都是在描述函數在多個執行流中調用的情況.
  • 線程安全 -> 線程
  • 可重入函數 -> 線程&信號處理函數
 
因此可重入的要求比線程安全的要求要更嚴格! 
  • 可重入函數一般情況下都是線程安全的.
  • 線程安全函數不一定是可重入的.

代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

pthread_mutex_t g_lock;
int g_count = 0;

//當前是線程安全,不可重入的
void Fun()
{
    pthread_mutex_lock(&g_lock);
    printf("lock\n");
    ++g_count;
    sleep(2);
    printf("unlock\n");
    pthread_mutex_unlock(&g_lock);
}

void* ThreadEntry(void* arg)
{
    (void)arg;
    while(1)
    {
        Fun();
    }
    return NULL;
}

void MyHandler(int sig)
{
    (void)sig;
    Fun();
}

int main()
{
    signal(SIGINT,MyHandler);
    pthread_mutex_init(&g_lock,NULL);

    pthread_t t1;
    pthread_create(&t1,NULL,ThreadEntry,NULL);
    pthread_join(t1,NULL);

    ThreadEntry(NULL);

    pthread_mutex_destroy(&g_lock);
    return 0;
}

當前的執行流程爲:
  1. main函數創建線程,並且調用線程入口函數ThreadEntry.
  2. 線程入口函數ThreadEntry內部死循環的調用Fun()函數
  3. Fun()函數進行 加鎖 -> 操作 -> 解鎖
如果在線程入口函數執行到加鎖以後,解鎖之前我們按下了 ctrl+c 會發生什麼情況呢?


當我們在ThreadEntry調用lock以後(unlock以前)按下 ctrl+c,這是會觸發信號捕捉函數,在信號捕捉函數裏會去調用 Fun() 函數.
但是調用Fun()函數的第一步就是解鎖,這時鎖已經被ThreadEntry函數獲取,那麼我們的信號捕捉函數就無法獲取鎖,只能等待鎖被釋放再去執行.
但是,之前在信號捕捉時候提到過,在執行完信號捕捉函數以前,主函數會一直阻塞的等待,知道信號捕捉函數退出,在接着執行.
這時候就出現了 ThreadEntry 在等待 MyHandler執行完, MyHandler 函數在等待ThreadEntry函數釋放鎖.所以就阻塞了.

因此對於這個場景下, Fun()函數是線程安全的, 但是不可重入.
發佈了77 篇原創文章 · 獲贊 50 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章