1. 程式人生 > >執行緒學習(三):執行緒的互斥

執行緒學習(三):執行緒的互斥

生產者與消費者模型

在講同步和互斥之前,首先了解一下消費者模型

什麼是消費者模型?

消費者模型是一個描述消費者和生產者之間的關係的一個模型,生產者和消費者模型指的是在一個場所中,兩個角色,三種關係

  1. 消費者和消費者之間——互斥 消費者之間是競爭關係,比如有一個雞腿,兩個人A和B去買,A買了,那麼B就不能買了,B得買下一個才行,反之B買了這個,A就要排隊等下一個。
  2. 生產者和消費者之間——同步+互斥 生產者和消費者之間講究一個時序性,同步其實也就是時序性,當超市沒有雞腿,消費者沒法買啊,只能等待生產者放上去才能買。同時,當生產者在放的時候你不能買,我都還沒放上去你就拿那不是搶劫嗎,所以需要互斥保護生產者放雞腿的這個操作。
  3. 生產者和生產者之間——互斥 放雞腿的時候,有兩廠商,但是你不能一起放啊,一起放我就記不住,你放一隻我放一隻,亂光了,就很難受,所以生產者一個在放你另一個別放,我好計數

為什麼需要執行緒同步和互斥

執行緒之間共享了程序之間的虛擬地址空間,因此缺乏資料訪問的控制,容易造成資料的混亂,為了保證資料的安全訪問,維持上面說的生產者和消費者模型的關係,因此提出了執行緒同步與互斥。

這裡指的資料一般是所有執行緒都能訪問到的資料才需要同步與互斥,其他執行緒內的區域性變數只屬於執行緒自身,其他執行緒無法訪問到,也就不存在資料安全問題了。

執行緒互斥

執行緒互斥解決的是資料的唯一訪問性,假設有一個進執行緒1訪問一個全域性資料,那麼什麼執行緒2、執行緒3或者執行緒0都靠邊站,等執行緒1訪問完了,其他執行緒再來競爭這個資料的訪問權,那麼資料就能夠保證安全訪問。

沒有執行緒互斥會怎麼樣

首先我們來看一個黃牛買票的例子

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

int ticket_left = 100;

void* ticket_scalper(void* id)
{
    int ticket_scalper_id = (int)id;
    while (1)
    {
        usleep
(100); if (ticket_left > 0) { ticket_left--; usleep(100); printf("No.%d ticket scalper buy a ticket, ticket left %d\n", ticket_scalper_id, ticket_left); } else { printf("no ticket left!!\n"); break; } } return NULL; } int main() { pthread_t tid[4] = {-1}; int i = 0; printf("%d tickets left.\n", ticket_left); for (i = 0; i < 4; i++) { printf("No.%d ticket scalper come to buy tickets~\n", i + 1); int ret = pthread_create(&tid[i], NULL, ticket_scalper, (void*)(i + 1)); if (ret < 0) { perror("pthread_create error"); return -1; } } for (i = 0; i < 4; i++) { pthread_join(tid[i], NULL); } return 0; }

執行結果 無互斥鎖保護 可以看到沒有互斥保護的話,有些黃牛買完票剩餘的票居然是一樣的,顯然是不可能的,所以我們需要一個互斥鎖來保護這個賣票的過程,一人買另一人不能買,一人買完另一人才能進去買。

mutex(互斥量)

保護變數在訪問時僅被單一執行緒訪問,做到變數的訪問安全,防止出現上面的情況,我們需要一個互斥量來完成下圖的做法 臨界操作

互斥量介面

int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
//初始化互斥量
//引數:
//	mutex:要初始化的互斥量
//	attr:NULL
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//	mutex:需要銷燬的互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex);//加鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解鎖

互斥量初始化有兩種方式

  1. 直接分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  1. 動態分配
使用pthread_mutex_init(mutex);分配

互斥量銷燬

  1. 使用PTHREAD_MUTEX_INITIALIAZER初始化的互斥量不需要銷燬
  2. 不要銷燬一個正處於加鎖狀態的互斥鎖
  3. 已經銷燬的互斥量,要確保後面不會有執行緒使用該鎖

互斥量的加鎖和解鎖

一般我們用lock(阻塞加鎖)和unlock加鎖和解鎖,當然加鎖還有其他介面,trylock非阻塞加鎖,timelock限時阻塞加鎖,可以根據需要選擇使用 使用lock加鎖的時候,可能會遇到以下情況:

  1. 互斥量處於未加鎖狀態,那麼會被呼叫lock的執行緒加鎖,返回成功
  2. 當互斥量已經處於加鎖狀態,或者在申請獲取加鎖和別的執行緒競爭失敗時,會陷入阻塞等待,等待解鎖才會加鎖

死鎖

死鎖條件

  1. 互斥條件——我拿了你不能拿
  2. 不可剝奪——我拿了你不能解鎖
  3. 請求與保持——拿了鎖1去獲取鎖2,沒有鎖2不釋放鎖1
  4. 環路等待——A拿了鎖1去請求鎖2,B拿了鎖2去請求鎖1

如何預防死鎖? 在死鎖條件中1、2是互斥量的正常使用,我們需要避免的是3、4情況,如何避免這兩種情況有艾茲格·迪傑斯特拉提出了著名的銀行家演算法來解決該問題。 在銀行中,客戶申請貸款的數量是有限的,每個客戶在第一次申請貸款時要宣告完成該專案所需的最大資金量,在滿足所有貸款要求時,客戶應及時歸還。銀行家在客戶申請的貸款數量不超過自己擁有的最大值時,都應儘量滿足客戶的需要。在這樣的描述中,銀行家就好比作業系統,資金就是資源,客戶就相當於要申請資源的程序。 如果需要詳細瞭解,請看維基百科:銀行家演算法

互斥鎖使用步驟

  1. 定義一個互斥鎖
  2. 初始化互斥鎖
  3. 對臨界操作進行加鎖
  4. 解鎖
  5. 重複3、4
  6. 當不再需要互斥鎖,銷燬互斥鎖

互斥量使用程式碼演示

//黃牛買票互斥鎖版本
//用互斥鎖實現同步,當一個黃牛(執行緒)買票時,其他黃牛(執行緒)不能擠進來
//一個黃牛開始買票,互斥鎖上鎖,其他黃牛無法獲得鎖,等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

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

void* sell_ticket(void* arg)
{
    int id = (int)arg;
    while (1)
    {
        usleep(100);
        pthread_mutex_lock(&mutex);
		//  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);
        //      限時阻塞加鎖,如果獲取不到鎖則等待指定時間,在這段
        //      時間內如果一直獲取不到,則報錯返回,否則加鎖
        if (ticket > 0)
        {
            ticket--;
            usleep(100);
            printf("Ticket scalper %d buy a ticket, tickets left %d\n", id, ticket);
        }
        else
        {
			//如果票賣完了也需要解鎖
			//如果不解鎖,那執行緒退出,鎖未解鎖,造成死鎖
			//後面執行緒無法獲取鎖狀態卡死
            printf("Ticket scalper %d want to buy a ticket, but no ticket left! -> %d\n", id, ticket);
            pthread_mutex_unlock(&mutex);
			//int pthread_mutex_unlock(pthread_mutex_t *mutex);
			//	解鎖
            pthread_exit(NULL);
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    pthread_t tid[4];
    int i, ret;
	pthread_mutex_init(&mutex, NULL);
	//int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    //      mutex: 互斥鎖變數
    //      attr:互斥鎖的屬性,NULL;
    //  	返回值:0-成功      errno-失敗
    printf("Train station has %d tickets left, start selling tickets\n", ticket);
    for (i = 0; i < 4; i++)
    {
        ret = pthread_create(&tid[i], NULL, sell_ticket, (void*)(i + 1));
        if (ret < 0)
        {
            perror("pthread_create failed\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++)
    {
        pthread_join(tid[i], NULL);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

黃牛賣票