1. 程式人生 > >線性(下)----執行緒安全

線性(下)----執行緒安全

執行緒安全

因為程序中執行緒共享了程序中的虛擬地址空間,所以執行緒間的通訊更加方便,但資料有可能存在爭搶關係,缺乏資料的訪問控制,多個執行緒併發容易造成資料混亂,所以資料安全訪問變得很重要。 造成資料混亂的的兩個經典模型

同步與互斥概念

同步:執行緒/程序之間對臨界資源的順序訪問關係(對臨界資源訪問的時序性) 互斥:執行緒/程序之間對臨界資源的同一時間的唯一訪問性關係

生產者與消費者模型

一個場所,兩個角色,三種關係 生產者與生產者的關係:互斥(來保證資料的安全操作) 生產者與消費者的關係:同步和互斥 消費者與消費者的關係:互斥

如何來解決執行緒中資料的安全訪問?------->實現執行緒間互斥

執行緒間的互斥實現:互斥鎖(互斥量) 執行緒間的同步實現:條件變數 POSIX訊號量:既可以實現同步可以實現互斥,既可以用於程序間的同步互斥,也可以實現執行緒間的同步互斥。

在互斥鎖中死鎖的必要條件?—如何避免 條件變數----等待和通知 為什麼條件變數和互斥鎖一起使用? 對於實現同步關鍵在於等待和通知,因為等待需要被喚醒,被喚醒的前提條件就是條件已經滿足,並且這個條件本身就是一個臨界資源。

互斥鎖(或互斥量)-----實現執行緒間的互斥

互斥鎖原理: 互斥鎖以排他的方式防止共享資料被併發訪問,是一個二元變數, 本質就是一個計數器,計數器只有0/1,在處理臨界資源時要先申請互斥鎖。互斥鎖處於開鎖狀態,申請到互斥鎖後立即佔有該鎖(加鎖),防止其他執行緒訪問資源。只有當前鎖定該互斥鎖的執行緒才可以釋放該互斥鎖。 在這裡插入圖片描述

互斥鎖操作介面

  1.定義一個互斥鎖//線上程建立之前完成
  定義一個互斥量(變數) pthread_mutex_t   name
  2.初始化互斥鎖
			
				互斥鎖的初始化有兩種方式:
					
					1.定義時賦值初始化,不需要手動釋放
							
						pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
						
					2.函式介面初始化,需要手動釋放

						int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
							//引數一 : 互斥鎖的變數
							//引數二 : 互斥鎖的屬性,一般設定為NULL

							返回0成功,返回非0就是錯誤								


3.對臨界操作程序加鎖或解鎖
加鎖:
int pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞式申請,如果鎖被鎖住則等待鎖被開啟,即若該鎖是鎖頂狀態,預設阻塞當前程序。	
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞加鎖,獲取不到鎖立即報錯返回 	
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec *restrict abs_timeout);	//限時阻塞加鎖,如果獲取不到鎖則指定等待時間,這段時間完了還沒獲取到,則報錯返回
	
解鎖(釋放):int pthread_mutex_unlock(pthread_mutex_t *mutex);//在任意一個有任何可能性退出的地方都要解鎖						
			

						
																
  4.銷燬互斥鎖
				pthread_mutex_destroy(pthread_mutex_t *restrict mutex);

死鎖情況:一直獲取不到鎖資源而造成的鎖死情況 死鎖產生的必要條件:必須具備以下條件才能滿足 全部具備以下條件: 1.互斥條件----一個獲取另外一個就不能獲取 2.不可剝奪條件----一個執行緒獲取鎖只能由這個執行緒自己釋放 3.請求與保持條件----獲取第一個鎖之後又去獲取第二個鎖 4.環路等待條件----a拿了鎖1去申請鎖2,而b拿了鎖2去申請鎖1,形成環路死鎖 如何預防產生死鎖:破壞死鎖產生的必要條件 避免產生死鎖:銀行家演算法(在這個演算法中定義了兩個狀態,安全狀態,非安全狀態,如果某一步操作操作完畢後處於安全狀態,那麼可以執行,如果處於不安全狀態那麼就不能執行)

線性間互斥例項:


/*
這是一個買票的例子
每一個黃牛都是一個執行緒,在這個例子中有一個總票數ticket
 *  每一個黃牛買到一張票這個ticket都會-1,直到票數為0
 */

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

int ticket = 100;

//互斥鎖的初始化有兩種方式:
//  1. 定義時直接賦值初始化,最後不需要手動釋放
//  2. 函式介面初始化,最後需要手動釋放
//  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex;   //定義互斥鎖

void *y_cow(void *arg)
{
    int id = (int)arg;
    while(1) {
        //2. 加鎖操作
        //  int pthread_mutex_lock(pthread_mutex_t *mutex);
        //      阻塞加鎖,如果獲取不到鎖則阻塞等待鎖被解開
        //  int pthread_mutex_trylock(pthread_mutex_t *mutex);
        //      非阻塞加鎖,如果獲取不到鎖則立即報錯返回EBUSY
        //  int pthread_mutex_timedlock (pthread_mutex_t *mutex,
        //          struct timespec *t);
        //      限時阻塞加鎖,如果獲取不到鎖則等待指定時間,在這段
        //      時間內如果一直獲取不到,則報錯返回,否則加鎖
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            usleep(100); //如果沒有進行加鎖操作,當票等於1時在睡眠的這個時間,很多執行緒都會進入,就會導致買到附屬的票
            printf("y_cow:%d get a ticket:%d!!\n", id, ticket);
            ticket--;
        }else {
            printf("have no ticket!!exit!!\n");
            //**加鎖後,在任意有可能退出的地方都要進行解鎖,
            //**否則會導致其他執行緒阻塞卡死
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
        //int pthread_mutex_unlock(pthread_mutex_t *mutex);
        //  解鎖
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t tid[4];
    int i = 0, ret;

    //1. int pthread_mutex_init(pthread_mutex_t *mutex, 
    //          const pthread_mutexattr_t *attr);
    //  互斥鎖的初始化
    //      mutex: 互斥鎖變數
    //      attr:互斥鎖的屬性,NULL;
    //  返回值:0-成功      errno-錯誤
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&tid[i], NULL, y_cow, (void*)i);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);
    //4. 銷燬互斥鎖
    pthread_mutex_destroy(&mutex);
    return 0;
}


條件變數—實現執行緒間的同步

條件變數的原理: 互斥鎖能夠解決對資源的互斥訪問,但有些情況互斥並不能解決

同步說的是對公共資源的時序訪問,若有資源,則一個執行緒就會來訪問,如果沒有資源則執行緒就會等待,條件變數發生改變時就會進行通知,執行緒就會做相應工作。所以條件變數用於等待某個條件被觸發。 在這裡以生產消費者模型來詳細說明一下同步: 在這裡插入圖片描述

條件變數不能單獨使用,需要和互斥鎖配合使用,因為執行緒等待被喚醒,被喚醒的前提是“條件改變了”,例如沒有產品時 ,消費者等待,有產品時,消費者被喚醒,有無產品就是“這個條件”。 執行緒同步實現程式碼:

/*  這是一個實現生產者與消費者同步的程式碼,生產者消費者分別代表一個執行緒
 *  有一個籃子,這個籃子是判斷條件,
 *  籃子裡有面
 *      代表消費者可以獲取面,通知生產者面已經取走了
 *      代表生產者需要等待
 *  籃子裡沒有面
 *      代表消費者等待
 *      代表生產者放面,通知消費者面已經放了
 */

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

//1. 定義條件變數
//  條件變數的初始化有兩種方式
//      1. 定義賦值初始化,不需釋放
//      2. 函式介面初始化, 需要釋放
//      pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond;   //定義條件變數
pthread_mutex_t mutex;  //定義互斥鎖
int basket = 0;         

//賣面的
void *sale_noddle(void *arg)
{
    while(1) {
        pthread_mutex_lock(&mutex);  //basket就是是一個判斷條件,執行緒都能訪問
		    //比如有多個生產者時(多個執行緒),對於這一個公共資料,那麼就會有爭搶行為,需要互斥鎖
        if (basket == 0) {           //加鎖實現了對這個全域性變數(公共資源的保護)
            printf("sale noddle!!!\n");
            basket == 1;  //生產了面,然後開始通知對方,喚醒消費者,使其不再等待
            //int pthread_cond_broadcast(pthread_cond_t *cond);
            //  喚醒所有等待在條件變數上的執行緒
            //int pthread_cond_signal(pthread_cond_t *cond);
            //  喚醒第一個等待在條件變數上的執行緒
            pthread_cond_signal(&cond);
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
void *buy_noddle(void *arg)
{
    while(1) {
        pthread_mutex_lock(&mutex);  //加鎖保護
        if (basket == 0) {
            //沒有面就要等待
            //int pthread_cond_wait(pthread_cond_t *cond,
            //          pthread_mutex_t *mutex);第二個引數就是互斥鎖 
            //  pthread_cond_wait的功能就是用來阻塞等待某個條件變數。它做的事情就是先解鎖然後進入等待
           
            //  pthread_cond_wait函式先對互斥鎖做了一個判斷是否加鎖,如果加鎖了就解鎖
            //  然後陷入等待*******整個過程是原子操作,不可被打斷。
            //
            //  要防止的情況就是:假如沒有面,而消費者又速度比較
            //  快,先拿到鎖了,那麼生產者將拿不到鎖,沒法生產將會
            //  造成雙方卡死
            //  所以如果消費者如果先獲取到鎖,那麼在陷入等待之前需
            //  要解鎖
            
            pthread_cond_wait(&cond, &mutex);
        }
        printf("buy noddles!!!\n");
        basket = 0;
        pthread_mutex_unlock(&mutex);  //解鎖
    }
    return NULL;
}
int main()
{
    pthread_t tid1, tid2;
    int ret;
    //1. 條件變數的初始化
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    ret = pthread_create(&tid1, NULL, sale_noddle, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    ret = pthread_create(&tid2, NULL, buy_noddle, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    //4. 條件變數的銷燬
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

POSIX 標準訊號量----既可以實現同步也可以實現互斥

即可用於程序也可以用於執行緒

POSIX訊號量實現執行緒間的同步和互斥

**訊號量本質:**具有一個等待佇列的計數器

執行緒同步實現: 消費者:沒有資源則等待 生產者:生產出來資源則通知等待佇列中的等待者

/*  這是驗證使用訊號量還實現執行緒間同步與互斥的程式碼
  訊號量的操作步驟:
 *          1. 訊號量的初始化
 *          2. 訊號量的操作(等待/通知)
 *          3. 訊號量的釋放
 *      1. 同步:等待與通知
 */

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

sem_t sem;
//執行緒間同步與互斥
void *thr_producer(void *arg)
{
    while(1) {
        //生產者
        sleep(1);
        printf("make a hot beef noddle!!\n");
        //生產出資源後要通知等待在訊號量上的執行緒/程序 
        //int sem_post(sem_t *sem);
        //訊號量修改的是自己內部的資源計數,這個內部的資源計數就是
        //條件,而條件變數修改的是外部的條件,需要我們使用者來修改
        sem_post(&sem);
    }
    return NULL;
}
void *thr_consumer(void *arg)
{
    while(1) {
        //消費者
        //2. 沒有資源則等待
        //阻塞等待,沒有資源則一直等待有資源,否則獲取資源
        //int sem_wait(sem_t *sem);
        //非阻塞等待,沒有資源則報錯返回,否則獲取資源
        //int sem_trywait(sem_t *sem);
        //限時等待,沒有資源則等待指定時長,這段時間內有資源則獲取
        //一直沒有資源則超時後報錯返回
        //int sem_timedwait(sem_t *sem,struct timespec *timeout);
        sem_wait(&sem);
        printf("very good!!!\n");
    }
    return NULL;
}
int ticket = 100;
void *buy_ticket(void *arg)
{
    while(1){
        //大家都是黃牛!!
        //因為計數器最大是1,也就代表只有一個執行緒能夠獲取到訊號量
        //這樣也就保證了同一時間只有一個執行緒能操作
        sem_wait(&sem);
        if (ticket > 0) {
            usleep(1000);
            ticket--;
            printf("cow %lu,buy a ticket:%d\n", ticket);
        }
        //操作完畢之後,對計數器進行+1,這時候訊號量資源計數就又可
        //以獲取了,然後又進入新一輪的資源爭搶,因為資源計數只有一
        //個,因此也只有一個執行緒能夠搶到
        sem_post(&sem);
    }
    return NULL;
}
int main()
{
    pthread_t tid1, tid2;
    int ret;

    //1. 初始化訊號量
    //int sem_init(sem_t *sem, int pshared, unsigned int value);
    //  sem:訊號量變數
    //  pshared:
    //          0-用於執行緒間
    //          非0-用於程序間
    //  value:訊號量的初始計數
    ret = sem_init(&sem, 0, 1);
    if (ret < 0) {
        printf("init sem error!!\n");
        return -1;
    }
    /*
    //建立生產者執行緒
    ret = pthread_create(&tid1, NULL, thr_producer, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    //建立消費者執行緒
    ret = pthread_create(&tid2, NULL, thr_consumer, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    */
    //黃牛買票執行緒
    pthread_t tid;
    int i = 0;
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&tid, NULL, buy_ticket, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }
    pthread_join(tid, NULL);
    //3. 銷燬訊號量
    //int sem_destroy(sem_t *sem);
    sem_destroy(&sem);
    return 0;
}


資源爭搶的另外一種模型------讀寫者模型(理解即可),實現讀寫模型的安全資料訪問是用—讀寫鎖

讀寫者模型: 大量讀,少量寫。 寫的時候他人不能讀, 讀的時候不能寫, 寫的時候他人不能寫, 讀的時候他人可以讀

互相關係 : 讀寫之間互斥 寫於寫之間互斥 讀和讀沒有關係 讀寫鎖的實現------讀寫鎖瞭解即可