1. 程式人生 > >【Linux】執行緒安全-同步與互斥

【Linux】執行緒安全-同步與互斥

執行緒安全:多個執行緒執行流對臨界資源的不安全爭搶操作

實現:如何讓執行緒之間安全對臨界資源進行操作就是同步與互斥

互斥:同一時間臨界資源的唯一訪問性

mutex(互斥量)

  • ⼤部分情況,執行緒使⽤的資料都是區域性變數,變數的地址空間線上程棧空間內,這種情況,變數歸屬單個執行緒,其他執行緒⽆法獲得這種變數。
  • 但有時候,很多變數都需要線上程間共享,這樣的變數稱為共享變數,可以通過資料的共享,完成執行緒之間的互動。
  • 多個執行緒併發的操作共享變數,會帶來⼀些問題。

我們先來看一個例子:操作共享變數的售票系統。

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


int ticket = 100;
void* route(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("pthread %d -> %d\n", id, ticket);
            ticket--;
        }
        else
            break;
    }

    return NULL;
}



int main()
{
    pthread_t tid[4];
    int i, ret;

    for(i = 0; i < 4; ++i)
    {
        ret = pthread_create(&tid[i], NULL, route, (void*)i);
        if(ret != 0)
            return -1;
    }

    for(i = 0; i < 4; ++i)
        pthread_join(tid[i], NULL);
    return 0;
}

很明顯,ticket都賣完了,還有執行緒在搶票。

  • if 語句判斷條件為真以後,程式碼可以併發的切換到其他執行緒
  • usleep這個模擬漫⻓業務的過程,在這個漫⻓的業務過程中,可能有很多個執行緒會進⼊該程式碼段
  • --ticket操作本⾝就不是⼀個原⼦操作

--操作並不是原⼦操作,⽽是對應三條彙編指令:

  • load:將共享變數ticket從記憶體載入到暫存器中
  • update: 更新暫存器⾥⾯的值,執⾏-1操作
  • store:將新值,從暫存器寫回共享變數ticket的記憶體地址。

要解決以上問題,需要做到三點:

  • 程式碼必須要有互斥⾏為:當代碼進⼊臨界區執⾏時,不允許其他執行緒進⼊該臨界區。
  • 如果多個執行緒同時要求執⾏臨界區的程式碼,並且臨界區沒有執行緒在執⾏,那麼只能允許⼀個執行緒進⼊該臨界區。
  • 如果執行緒不在臨界區中執⾏,那麼該執行緒不能阻⽌其他執行緒進⼊臨界區。

要做到這三點,本質上就是需要⼀把鎖。Linux上提供的這把鎖叫互斥量。

互斥量的介面:

初始化互斥量

初始化互斥量有兩種⽅法:

  • ⽅法1,靜態分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 
  • ⽅法2,動態分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
 引數:
 mutex:要初始化的互斥量
 attr:NULL

銷燬互斥量

銷燬互斥量需要注意:

  • 使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷燬
  • 不要銷燬⼀個已經加鎖的互斥量
  • 已經銷燬的互斥量,要確保後⾯不會有執行緒再嘗試加鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加鎖和解鎖

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>
#include<string.h>


int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf("pthread %d -> %d\n", id, ticket);
            ticket--;
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}



int main()
{
    pthread_t tid[4];
    int i, ret;

    pthread_mutex_init(&mutex, NULL);
    for(i = 0; i < 4; ++i)
    {
        ret = pthread_create(&tid[i], NULL, route, (void*)i);
        if(ret != 0)
            return -1;
    }

    for(i = 0; i < 4; ++i)
        pthread_join(tid[i], NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}

很明顯,加鎖之後執行緒安全得到了保證。至於只有一個執行緒訪問,這與時間片有關。

死鎖:

產生死鎖的原因主要是:
(1) 因為系統資源不足。
(2) 程序執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。
產生死鎖的四個必要條件:
(1) 互斥條件:一個資源每次只能被一個程序使用。
(2) 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
死鎖的解除與預防:
理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和
解除死鎖。所以,在系統設計、程序排程等方面注意如何不讓這四個必要條件成立,如何確
定資源的合理分配演算法,避免程序永久佔據系統資源。此外,也要防止程序在處於等待狀態
的情況下佔用資源。因此,對資源的分配要給予合理的規劃。

避免死鎖:銀行家演算法,死鎖檢測演算法

同步:對臨界資源操作的時序可控性

條件變數:等待、喚醒

條件變數一共提供了兩個功能,一個是等待,一個是喚醒

對於一個外部條件進行判斷,如果條件滿足則繼續操作;如果條件不滿足怎等待

為了能夠讓程式繼續操作,需要其他執行流修改條件,是滿足條件,並喚醒對方

這裡所說的外部條件,條件變數不會提供是我們使用者設定的判斷依據

條件變數

  • 當⼀個執行緒互斥地訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什麼也做不了。
  • 例如⼀個執行緒訪問佇列時,發現佇列為空,它只能等待,只到其它執行緒將⼀個節點新增到佇列中。這種情況就需要⽤到條件變數

條件變數函式

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
引數:
 cond:要初始化的條件變數
 attr:NULL

銷燬

int pthread_cond_destroy(pthread_cond_t *cond)

等待條件滿⾜

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
引數:
 cond:要在這個條件變數上等待
 mutex:互斥量,後⾯詳細解釋

喚醒等待

喚醒所有等待
int pthread_cond_broadcast(pthread_cond_t *cond);
喚醒單個等待
int pthread_cond_signal(pthread_cond_t *cond);

示例:

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

pthread_cond_t sale;
pthread_cond_t eat;
pthread_mutex_t mutex;
int have_noodle = 0;

void* sale_noodle(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(have_noodle == 1)
            pthread_cond_wait(&sale, &mutex);
        printf("pthread %d create noodle!!\n", id);
        have_noodle = 1;

        //生產出來後通知買方便麵的人
        pthread_cond_signal(&eat);
        pthread_mutex_unlock(&mutex);

    }

    return NULL;
}

void* eat_noodle(void* arg)
{
    while(1)
    {
        //因為have_noodle的操作也是一個臨界資源的操作,因此需要被
        //保護,使用互斥鎖進行保護
        pthread_mutex_lock(&mutex);
        if (have_noodle == 0) 
        {
            //因為等待時間不確定,因此有可能會浪費很多等待時間
            //因此使用環境變數提供的死等操作,但是這個死等需要能夠
            // 被喚醒,這樣的話,一旦方便麵生產出來直接喚醒我們的死
            //等,不會浪費多餘的等待時間
            //防止不滿足條件陷入休眠,沒有解鎖,對方無法獲取鎖,沒
            //辦法生產方便麵,因此需要解鎖,
            //但是解鎖和休眠必須是原子操作
             pthread_cond_wait(&eat, &mutex);
            //被喚醒,這時候可以繼續吃麵,並且修改條件,但是條件是
            //臨界資源,因此需要加鎖,
            // pthread_cond_wait整體操作
            // 解鎖-》休眠-》被喚醒後加鎖(但是這不是一個阻塞操作,
            //而是直接計數器置0)

        }
        printf("eat noodle!! good!!\n");
        have_noodle = 0;
        pthread_mutex_unlock(&mutex);
        //吃完之後通知一下賣方便麵的
        pthread_cond_signal(&sale);
    }
}

int main()
{
    pthread_t tid1, tid2;
    int ret;

    //條件變數初始化
    pthread_cond_init(&eat, NULL);
    pthread_cond_init(&sale, NULL);
    pthread_mutex_init(&mutex, NULL);

    ret = pthread_create(&tid1, NULL, sale_noodle, (void*)1);
    if(ret != 0)
        return -1;
    ret = pthread_create(&tid1, NULL, sale_noodle, (void*)2);
    if(ret != 0)
        return -1;
    ret = pthread_create(&tid2, NULL, eat_noodle, NULL);
    if(ret != 0)
        return -1;

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    //條件變數銷燬
    pthread_cond_destroy(&eat);
    pthread_cond_destroy(&sale);
    pthread_mutex_destroy(&mutex);
    return 0;
}

為什麼pthread_ cond_ wait 需要互斥量?

  • 條件等待是執行緒間同步的⼀種⼿段,如果只有⼀個執行緒,條件不滿⾜,⼀直等下去都不會滿⾜,所以必須要有⼀個執行緒通過某些操作,改變共享變數,使原先不滿⾜的條件變得滿⾜,並且友好的通知等待在條件變數上的執行緒。
  • 條件不會⽆緣⽆故的突然變得滿⾜了,必然會牽扯到共享資料的變化。所以⼀定要⽤互斥鎖來保護。沒有互斥鎖就⽆法安全的獲取和修改共享資料。

按照上⾯的說法,我們設計出如下的程式碼:先上鎖,發現條件不滿⾜,解鎖,然後等待在條件變數上不就⾏了,如下程式碼:

// 錯誤的設計
 pthread_mutex_lock(&mutex);
 while (condition_is_false) {
 pthread_mutex_unlock(&mutex);
 //解鎖之後,等待之前,條件可能已經滿⾜,訊號已經發出,但是該訊號可能被錯過
 pthread_cond_wait(&cond);
 pthread_mutex_lock(&mutex);
 }
 pthread_mutex_unlock(&unlock);
  • 由於解鎖和等待不是原⼦操作。調⽤解鎖之後,pthread_ cond_ wait之前,如果已經有其他執行緒獲取到互斥量,摒棄條件滿⾜,傳送了訊號,那麼pthread_ cond_ wait將錯過這個訊號,可能會導致執行緒永遠阻塞在這個pthread_ cond_ wait。所以解鎖和等待必須是⼀個原⼦操作。
  • nt pthread_ cond_ wait(pthread_ cond_ t *cond,pthread_ mutex_ t * mutex); 進⼊該函式後,會去看條件量等於0不?等於,就把互斥量變成1,直到cond_ wait返回,把條件量改成1,把互斥量恢復成原樣。

條件變數使⽤規範

  • 等待條件程式碼
pthread_mutex_lock(&mutex);
 while (條件為假)
 pthread_cond_wait(cond, mutex);
 修改條件
 pthread_mutex_unlock(&mutex);
  • 給條件傳送訊號程式碼
pthread_mutex_lock(&mutex);
 設定條件為真
 pthread_cond_signal(cond);
 pthread_mutex_unlock(&mutex);