Linux——線程深度剖析(二),拿下線程安全

互斥與同步

  • 互斥是指散佈在不同進程之間的若干程序片斷,當某個進程運行其中一個程序片段時,其它進程就不能運行它 們之中的任一程序片段,只能等到該進程運行完這個程序片段後纔可以運行。

  • 同步是散佈在不同進程之間的若干程序片斷,它們的運行必須嚴格按照規定的 某種先後次序來運行,這種先後次序依賴於要完成的特定的任務。

臨界資源與臨界區

  • 臨界資源:多個線程共享的資源

  • 臨界區:多個線程訪問臨界資源的代碼段

線程安全

因爲進程中的線程共享了進程的虛擬地址空間,因此線程間通信將變得更加簡單,但是缺點也隨之而來:缺乏數據的訪問控制容易造成數據混亂(因爲大家都在爭搶訪問公共資源),導致程序結果出現二義性。

舉個例子:

  • 在一個進程中有兩個線程,線程A,線程B,兩個線程同時對一個整形的全局變量a=10進程++操作,預期結果a=12;
  • 當線程A拿到CPU之後,對全局a進行++操作,但是該操作是非原子性的,當線程A將10從內存中讀取到寄存器中後,時間片到期,此時線程A就被切換出去了,此時a的值在內存中不變。
  • 搶佔式執行的前提下,假設線程B在這個時候拿到了CPU資源,完成了++操作且未被打斷,並將11寫回到了內存當中去。
  • 這時候,線程A重新擁有資源,根據程序計數器(保存下一條執行的指令),上下文信息(寄存器中的值),恢復現場,此時線程A從寄存器中拿到的值爲10,並完成++操作,將11寫回內存。

結果與我們的初衷完全不一樣,這就是線程完全問題。

如何保證線程安全

  • 通過互斥鎖,讓線程之間互斥

互斥鎖

  • 在互斥鎖內部有一個計數器,名爲互斥量。計數器的取值只能爲0或爲1。
  • 當計數器取值爲0時,表示當前線程無法獲得互斥鎖,無法訪問臨界資源
  • 當計數器取值爲1時,表示當前線程可以獲取互斥鎖,可以訪問臨界資源。

互斥鎖中的計數器如何保證原子性?
在這裏插入圖片描述

獲取資源的時候:
1.寄存器中的值賦值爲0
2.將寄存器中的值與內存的值交換
3.判斷寄存器的值,得出結果


在這裏插入圖片描述

在第二步交換的過程中,系統通過xchgb交換操作,該操作是彙編代碼,一次性完成,該操作保證了原子性。

1.互斥鎖的初始化

 int pthread_mutex_init(pthread_mutex_t *_mutex,_const pthread_mutex_mutexattr_t* _mutexattr)
  • mutex:傳入互斥鎖變量的地址,pthread_mutex_init會初始化互斥變量
  • attr:屬性,一般傳遞NULL,採用默認屬性
  • pthread_mutex_init:互斥鎖變量的類型

2.加鎖

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:傳入互斥鎖變量的地址。
  • 如果mutex當中的計數器的值爲1,則pthread_mutex_lock接口就返回了,表示說加鎖成功,同時計數器當中值會被更改爲0,
  • 如果mutex當中的計數器的值爲0,則pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口沒有返回,阻塞在該函數的內部,直到加鎖成功

3.解鎖

int pthread_mutex_unlock(pthread_mutex_t*mutex)
  • 不管是哪一個加鎖接口加鎖成功的,都可以使用該接口進行解鎖
  • 解鎖的時候,會將互斥鎖變量當中的計數器的值,從0變成1,表示其他線程可以獲取互斥鎖

4.互斥鎖的銷燬

pthread_mutex_destroy (pthread_mutex_t*);

實例的運用:黃牛搶票

創建四個線程,每個線程在搶票時候進行加鎖,結束後解鎖。

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

#define THREADCOUNT 4

int g_tickets = 100;
pthread_mutex_t lock_;

void* MyStartThread(void* arg)
{
   
   
    (void)arg;
    while(1)
    {
   
   
        pthread_mutex_lock(&lock_);
        if(g_tickets > 0)
        {
   
   
            printf("i am thread %p, i have ticket %d\n", pthread_self(), g_tickets);
            g_tickets--;
        }
        else
        {
   
   
            pthread_mutex_unlock(&lock_);
            break;
        }
        pthread_mutex_unlock(&lock_);
    }
    return NULL;
}

int main()
{
   
   
    pthread_mutex_init(&lock_, NULL);
    pthread_t tid[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
   
   
        int ret = pthread_create(&tid[i], NULL, MyStartThread, NULL);
        if(ret < 0)
        {
   
   
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
   
   
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&lock_);
    return 0;
}

在這裏插入圖片描述
注意:

  • 在任何開始訪問臨界資源的地方進行加鎖
  • 在所有可能線程退出的地方進行解鎖,否則可能造成死鎖

條件變量(PCB等待隊列)

它可以使用在資源不滿足的情況下處於休眠狀態,讓出CPU資源,而資源狀態滿足時喚醒線程

1.初始化

int pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr)
  • cond : pthread_cond_t :條件變量的類型,傳參的時候還是傳入條件變量的地址
  • attr:條件變量的屬性,通常傳遞NULL,採用默認屬性

2.等待:將調用該接口的線程放到PCB等待隊列當中

int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
  • cond:條件變量
  • mutex:互斥鎖
  • pthread_cond_wait會在內部進行解互斥鎖,且順序是:先放入PCB等待隊列,再進行解鎖
  • 接口內部實現的邏輯:1.從PCB等待隊列中移除。 2.搶佔互斥鎖 如果沒有搶到互斥鎖,那將會阻塞在pthread_cond_wait的內部搶鎖邏輯當中

3.喚醒

int pthread_cond_siganl(pthread_cond_t* cond)
  • 通知PCB等待隊列當中的線程,將其從隊列當中出來,喚醒該線程
  • 喚醒至少一個PCB等待隊列當中的線程
int pthread_cond_broadcast (pthread_cond_t*cond)
  • 喚醒PCB等待隊列當中全部的線程

4.釋放

int pthread_cond_destroy (pthread_cond_t* cond)

結合條件變量實戰一下消費者模型:消費者吃麪,生產者做面,當碗裏沒面時,將消費真放進pcb等待隊列,做好後有生產者通知其出隊,當碗裏有面的時,將生產者放入pcb等待隊列,當碗裏喫完的時候由消費者通知其做面。

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

#define THREADCOUNT 2

int g_bowl = 0;
pthread_mutex_t g_mut;
pthread_cond_t g_cond;

void* EatStart(void* arg)
{
   
   
    while(1)
    {
   
   
        //eat
        pthread_mutex_lock(&g_mut);
        while(g_bowl <= 0)
        {
   
   
            pthread_cond_wait(&g_cond, &g_mut);
        }
        printf("i am %p, i eat %d\n", pthread_self(), g_bowl);
        g_bowl--;
        pthread_mutex_unlock(&g_mut);
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

void* MakeStart(void* arg)
{
   
   
    while(1)
    {
   
   
        //make
        pthread_mutex_lock(&g_mut);
        while(g_bowl > 0)
        {
   
   
            pthread_cond_wait(&g_cond, &g_mut);
        }
        g_bowl++;
        printf("i am %p, i make %d\n", pthread_self(), g_bowl);
        pthread_mutex_unlock(&g_mut);
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

int main()
{
   
   
    pthread_mutex_init(&g_mut, NULL);
    pthread_cond_init(&g_cond, NULL);
    pthread_t consume[THREADCOUNT], product[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
   
   
        int ret = pthread_create(&consume[i], NULL, EatStart, NULL);
        if(ret < 0)
        {
   
   
            perror("pthread_create");
            return -1;
        }

        ret = pthread_create(&product[i], NULL, MakeStart, NULL);
        if(ret < 0)
        {
   
   
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
   
   
        pthread_join(consume[i], NULL);
        pthread_join(product[i], NULL);
    }

    pthread_mutex_destroy(&g_mut);
    pthread_cond_destroy(&g_cond);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章