1. 程式人生 > >Linux多執行緒——使用互斥量同步執行緒

Linux多執行緒——使用互斥量同步執行緒

前文再續,書接上一回,在上一篇文章:Linux多執行緒——使用訊號量同步執行緒中,我們留下了一個如何使用互斥量來進行執行緒同步的問題,本文將會給出互斥量的詳細解說,並用一個互斥量解決上一篇文章中,要使用兩個訊號量才能解決的只有子執行緒結束了對輸入的處理和統計後,主執行緒才能繼續執行的問題。 一、什麼是互斥量 互斥量是另一種用於多執行緒中的同步訪問方法,它允許程式鎖住某個物件,使得每次只能有一個執行緒訪問它。為了控制對關鍵程式碼的訪問,必須在進入這段程式碼之前鎖住一個互斥量,然後在完成操作之後解鎖。 二、互斥量的函式的使用 它們的定義與使用訊號量的函式非常相似,它們的定義如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_destroy(pthread_mutex_t *mutex);
它們的意義就如它們的名字所示的那樣,成功時返回0,失敗時返回錯誤程式碼,它們並不設定errno。 pthread_mutex_init函式中的引數mutexattr指定互斥量的屬性,在這裡我們並不關心互斥量的屬性,所以把它設定為NULL,使用預設屬性即可。同樣的,pthread_mutex_lock和pthread_mutex_unlock都是原子操作,如果一個執行緒呼叫pthread_mutex_lock試圖鎖住互斥量,而該互斥量,又被其他執行緒鎖住(佔用),則該執行緒的pthread_mutex_lock呼叫就會阻塞,直到其他執行緒對該互斥量進行解鎖,該執行緒才能獲得該互斥量,pthread_mutex_lock呼叫才會返回。
注意,使用互斥量的預設屬性,如果程式試圖對一個已經加鎖的互斥量呼叫pthread_mutex_lock,程式就會阻塞,而又因為擁有互斥量的這個執行緒正是現在被阻塞的執行緒,所以這個互斥量就永遠不會被解鎖,也就是說,程式就會進入死鎖的狀態。在使用時要多加註意,確保在同一個執行緒中,對加鎖的互斥再次進行加鎖前要對其進行解鎖。 三、使用互斥量進行執行緒同步 下面以一個簡單的多執行緒程式來演示如何使用互斥量來進行執行緒同步。在主執行緒中,我們建立子執行緒,並把陣列msg作為引數傳遞給子執行緒,然後主執行緒呼叫函式pthread_mutex_lock對互斥量加鎖,等待輸入,輸入完成後,呼叫函式pthread_mutex_unlock對互斥量解鎖,從而使執行緒函式中的對互斥量加鎖的pthread_mutex_lock函式返回並執行子執行緒中的程式碼。執行緒函式在把字串的小寫字母變成大寫並統計輸入的字元數量之後,它呼叫pthread_mutex_unlock對互斥量解鎖,使主執行緒能夠繼續獲得互斥量(即對其加鎖函式返回),再次執行輸入功能直到主執行緒再次呼叫pthread_mutex_unlock對其解鎖,一直如此重複,直到輸入end。 原始檔為lockthread.c,原始碼如下:
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>


//宣告執行緒函式和互斥量
void* thread_func(void *msg);
pthread_mutex_t mutex;


#define MSG_SIZE 512


int main()
{
	int res = -1;
	pthread_t thread;
	void *thread_result = NULL;
	char msg[MSG_SIZE] = {'\0'};
	//初始化互斥量,使用預設的互斥量屬性
	res = pthread_mutex_init(&mutex, NULL);
	if(res != 0)
	{
		perror("pthread_mutex_init failed\n");
		exit(EXIT_FAILURE);
	}
	//建立子執行緒,並把msg作為執行緒函式的引數傳遞給thread_func
	res = pthread_create(&thread, NULL, thread_func, msg);
	if(res != 0)
	{
		perror("pthread_create failed\n");
		exit(EXIT_FAILURE);
	}
	//輸入字串,以串‘end’結束
	printf("Input some test. Enter 'end' to finish\n");
	//把互斥量mutex加鎖,以確保同一時間只有該執行緒可以訪問msg中的資料
	pthread_mutex_lock(&mutex);
	while(strcmp("end\n", msg) != 0)
	{
		if(strncmp("TEST", msg, 4) == 0)
		{
			strcpy(msg, "copy_data\n");
		}
		else
		{
			fgets(msg, MSG_SIZE, stdin);
		}
		//把互斥量mutex解鎖,讓其他的執行緒可以訪問msg中的資料
		pthread_mutex_unlock(&mutex);
		sleep(1);//休眠1秒再繼續迴圈,讓其他執行緒有執行的機會
		pthread_mutex_lock(&mutex);
	}
	pthread_mutex_unlock(&mutex);
	printf("\nWaiting for thread finish...\n");
	//等待子執行緒結束
	res = pthread_join(thread, &thread_result);
	if(res != 0)
	{
		perror("pthread_join failed\n");
		exit(EXIT_FAILURE);
	}
	printf("Thread joined\n");
	//清理互斥量
	pthread_mutex_destroy(&mutex);
	exit(EXIT_SUCCESS);
}
void* thread_func(void *msg)
{
	int i = 0;
	char *ptr = msg;
	sleep(1);
	//把互斥量mutex加鎖,以確保同一時間只有該執行緒可以訪問msg中的資料
	pthread_mutex_lock(&mutex);
	while(strcmp("end\n", msg) != 0)
	{
		//把小寫字母變成大寫
		for(i = 0; ptr[i] != '\0'; ++i)
		{
			if(ptr[i] >= 'a' && ptr[i] <='z')
			{
				ptr[i] -= 'a' - 'A';
			}
		}
		printf("You input %d characters\n", i-1);
		printf("To uppercase: %s\n", ptr);
		//把互斥量mutex解鎖,讓其他的執行緒可以訪問msg中的資料
		pthread_mutex_unlock(&mutex);
		sleep(1);//休眠1秒再繼續迴圈,讓其他執行緒有執行的機會
		pthread_mutex_lock(&mutex);
	}
	pthread_mutex_unlock(&mutex);
	//退出執行緒
	pthread_exit(NULL);
}
執行結果如下:
程式分析: 這個程式的工作流程已經說得非常清楚了,這裡先來說說在main函式和執行緒函式thread_func中while迴圈中的sleep(1)語句的作用。可能很多人會認為這個sleep(1)是為了讓子執行緒完成其處理和統計功能,所以要讓主執行緒休眠1秒鐘來等待子執行緒的處理統計工作的完成。的確在這裡子執行緒進行的工作十分簡單,1秒鐘內的確可以處理統計完畢。但是這裡的sleep(1)並不是為了實現這個功能,這兩個迴圈中的sleep(1)是為了讓其他的執行緒有機會被執行到,如果在一次的加鎖和解鎖之間沒有這條語句的話,則當前的執行緒將會一直在迴圈中獲得互斥量,因為其他的執行緒沒有執行它的程式碼的時間,所以就要用這樣的一條語句來給其他的執行緒一個執行的機會。如果子執行緒的執行時間超過1秒,這個程式還是會正常執行。 以這個例子來說,在主執行緒中,當輸入資料完畢並對互斥量解鎖之後,並不馬上迴圈對其加鎖,此時子執行緒就有了執行的機會,它會對互斥量進行加鎖,同樣地,當它處理統計完輸入的資料後,它在進入下一次迴圈前,也休眠1秒,讓主執行緒有機會再次執行。而主執行緒什麼時候能夠執行,取決於子執行緒何時對互斥量進行解鎖。因為如果子執行緒擁有(鎖住)互斥量,則主執行緒中函式pthread_mutex_lock就不會返回,使主執行緒處於阻塞狀態。 換句話來說,就是只有子執行緒結束了對輸入的處理和統計後,主執行緒才能繼續執行,向msg中寫入資料。看到這裡,你應該知道之前在使用訊號量時,我們多用一個訊號量也是為了達到這個目的。所以當我們輸入TEST時,程式有兩個輸入,但還是能正常執行,同樣解決了之前使用一個訊號量時所帶來的問題。 訊號量和互斥量的作用都是保護程式碼段的互斥裝置,它們也非常相似。但在本例中,與使用訊號量相比,實現同樣的功能,如果使用訊號量的話,則需要兩個訊號量,而使用互斥量的話,只需要一個。可以說在本例中,使用互斥量更簡單。但是我覺得使用互斥量更容易犯錯,我們可以看到在這個例子中,我們需要使用sleep語句來讓其他執行緒獲得執行的機會,但是在使用訊號量的程式,它並不需要使用sleep,相對來說比較直觀。我知道可能是我的實現方法不好,但是對於使用互斥量來說,我想了很久也想不到不使用sleep的方法。