1. 程式人生 > 實用技巧 >執行緒同步圖解 - 互斥量

執行緒同步圖解 - 互斥量

執行緒同步問題廣泛應用於多種場景下,特別是與網路資料收發等耗時操作有關的場景。執行緒的操作往往比較抽象,且大多執行在程式的後臺,無法直觀的觀察其執行狀態,因此,本文以圖解的形式,為讀者講述執行緒同步的原理,並附以相關例程方便大家除錯。

本文講述互斥量(也叫互斥鎖pthread_mutex_t)執行緒同步原理,互斥量作為比較簡單的執行緒同步方式,在實際的併發問題中會經常碰到。

更多執行緒同步的例程見[公眾號:斷點實驗室]的其他系列文章
執行緒同步圖解 - 互斥量
執行緒同步圖解 - 條件變數

例如,當我們在程序中通過幾個執行緒以併發方式完成特定任務時,可能會出現這幾個執行緒需要同時操作一個數據(即臨界區資源,類似於搶火車票場景),若不對臨界區資源進行必要的保護,將會操作資料錯亂問題

互斥量用於保證對多個執行緒共享的臨界區資料操作的完整性(原子性),即同一時刻只能有一個執行緒持有互斥量,而且只有這個執行緒可以對互斥量解鎖,當無法獲取互斥量時,其他執行緒進入睡眠等待狀態。

1、執行緒同步原理

通過互斥量來實現執行緒對臨界區資源同步訪問的操作一般可分為三步(沒錯,操作步驟和把大象放進冰箱裡是一樣的),即

加鎖 -> 訪問臨界區資源 -> 解鎖

既然是執行緒同步圖解,這裡我們給出原理圖來描述整個執行緒同步的過程

假設producer執行緒首先開始執行,producer執行緒同時鎖定互斥量(圖中紅點表示互斥量),取得了對臨界區資源的獨佔使用權,然後執行其執行緒函式操作臨界區資源,最後釋放互斥量,完成一輪對臨界區資源的操作

consumer執行緒晚於producer執行緒啟動,由於互斥量已被producer執行緒鎖定,因此consumer執行緒將投入休眠狀態,直到重新獲得互斥量恢復執行

若consumer執行緒首先執行,情況與producer執行緒先開始執行的情況類似,同樣是按照鎖定互斥量,執行執行緒邏輯,釋放互斥量這三個操作進行,若臨界區資源滿足條件,則執行consumer的執行緒邏輯,如圖所示的從後臺快取佇列中提取資料

若後臺快取佇列為空,則consumer執行緒邏輯不滿足執行條件,則直接釋放互斥量,然後再次和producer執行緒重新展開對臨界區資源獨佔的競爭,直到臨界區資源滿足執行緒執行邏輯為止

2 例程原始碼清單

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

static int val=0;//臨界區資源,critical resource
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//靜態初始化互斥鎖

//生產者臨界區資源處理函式
void producer_proc(int *cr){
	printf("producer_process_critical_resource++++++++++++++++++%d\n\n",val);
	*cr+=1;//處理臨界區資源,做+1簡單處理
}

//消費者臨界區資源處理函式
void consumer_proc(int *cr){
	printf("consumer_process_critical_resource------------------%d\n\n",val);
	if(*cr>=10){
		*cr=0;//處理臨界區資源,這裡直接重置為0
	}
	
}

//生產者執行緒函式
void *producer_fun(void *p) {
	while(1){
		printf("producer_before_lock++++++++++++++++++++++++++++++++%d\n\n",val);
		pthread_mutex_lock(&lock);//取得互斥鎖
		printf("producer_locked+++++++++++++++++++++++++++++++++++++%d\n\n",val);

		//處理臨界區資源,做+1簡單處理
		producer_proc(&val);

		printf("producer_unlock+++++++++++++++++++++++++++++++++++++%d\n\n",val);
		pthread_mutex_unlock(&lock);//釋放互斥鎖
		sleep(1);
	}//end for while
    return NULL;
}

//消費者執行緒函式
void *consumer_fun(void *p) {
	while(1){
		printf("consumer_before_lock--------------%d\n\n",val);
		pthread_mutex_lock(&lock);//取得互斥鎖
		printf("consumer_locked-------------------%d\n\n",val);

		//處理臨界區資源,這裡直接重置為0
		consumer_proc(&val);

		printf("consumer_unlock-----------------------%d\n\n",val);
		pthread_mutex_unlock(&lock);
		sleep(1);
	}//end for while
    return NULL;
}

int main(int argc,char *argv[]) {
	pthread_t cid,pid;//執行緒id

	//建立消費者執行緒
	int ret=pthread_create(&cid,NULL,consumer_fun,NULL);
	if(ret!=0) {//檢查執行緒建立結果
		printf("create consumer thread failed\n");
		exit(1);
	}

	//建立生成者執行緒
	ret=pthread_create(&pid,NULL,producer_fun,NULL);
	if(ret!=0) {//檢查執行緒建立結果
		printf("create producer thread failed\n");
		exit(1);
	}

	//等待執行緒結束
	pthread_join(cid,NULL);
	pthread_join(pid,NULL);

	return 0;
} 

3 例程編譯執行分析

下面給出例程的編譯、執行以及結果的分析

3.1 例程編譯

下面給出例程的編譯腳步,編譯過程非常簡單,在原始碼及Makefile編譯指令碼目錄執行[make]命令,即可完成例程編譯,執行下面的操作即可開始例程的執行。

Makefile編譯指令碼

test: main.c
	gcc -o test -g3 main.c -l pthread

clean:
	rm test

程式碼編譯執行

make
./test

3.2 例程執行結果

producer_before_lock++++++++++++++++++++++++++++++++4

producer_locked+++++++++++++++++++++++++++++++++++++4

consumer_before_lock--------------4

producer_process_critical_resource++++++++++++++++++4

producer_unlock+++++++++++++++++++++++++++++++++++++5

consumer_locked---------------------5

consumer_process_critical_resource------------------5

3.3 執行結果分析

從上面的執行結果可以看出

producer執行緒在已鎖定互斥量的情況下(producer_lock),獲得了對臨界區資源的獨佔使用權

consumer執行緒也試圖鎖定互斥量(consumer_before_lock),但此時互斥量已被producer執行緒鎖定,因此consumer執行緒將阻塞等待(consumer_lock未執行),直到互斥量被釋放為止

producer執行緒繼續執行自己的邏輯,處理臨界區資源(producer_process_critical_resource)

producer執行緒執行完畢後,釋放互斥量(producer_unlock)

因為此時等待互斥量的執行緒只有一個,因此consumer執行緒取得互斥量(consumer_lock)並恢復執行,然後執行自己的邏輯(consumer_process_critical_resource)

若此時存在多個執行緒同時等待互斥量,那麼當互斥量被釋放後,將出現多個執行緒競爭同一個互斥量的場景,最終哪個執行緒能夠鎖定互斥量將取決於核心的排程

若執行緒釋放互斥量並以廣播形式喚醒其他休眠中的執行緒,此時將引發驚群效應,即多個執行緒幾乎同時醒來,但只有其中一個執行緒可以獲得互斥量並執行自己的邏輯,其他未獲得互斥量的執行緒只能繼續進入休眠狀態

驚群效應是應該盡力避免的,因為這種執行緒同步方式效率低,且浪費處理器資源及時間,驚群效應會在後續的內容中為大家講解,如果有人感興趣的話

由於執行緒的排程存在隨機性,每次執行的時序可能不完全相同,因此讀者在自己的環境中執行的結果可能和這裡貼出來的結果不完全相同,這是正常的,即使是同一套程式碼每次執行的結果也有一定的隨機性

4 互斥量同步方式的問題

採用互斥量的方式保護快取佇列臨界區資源,若快取佇列為空,則consumer執行緒邏輯不滿足執行條件,則直接釋放互斥量並退出,然後再次和producer執行緒重新展開對臨界區資源獨佔的競爭。

由於執行緒排程的隨機性,若consumer執行緒在臨界區資源不滿足執行條件的情況下,再次獲得優先執行權,那麼執行緒邏輯會直接退出本次呼叫,這種情況浪費了寶貴的處理器資源及時間,因此,應該採用其他效率更高的同步方式來操作執行緒同步。

更多執行緒同步的例程見[公眾號:斷點實驗室]的其他系列文章
執行緒同步圖解 - 互斥量
執行緒同步圖解 - 條件變數


// 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視訊開發、影象處理、網路、
// Linux,Windows、Android、嵌入式開發等