1. 程式人生 > >linux多執行緒程式設計——同步與互斥

linux多執行緒程式設計——同步與互斥

我們在前面文章中已經分析了多執行緒VS多程序,也分析了執行緒的使用,現在我們來講解一下linux多執行緒程式設計之同步與互斥。

現在,我們不管究竟是多執行緒好還是多程序好,先講解一下,為什麼要使用多執行緒?

一、 為什麼要用多執行緒技術?

1、避免阻塞,大家知道,單個程序只有一個主執行緒,當主執行緒阻塞的時候,整個程序也就阻塞了,無法再去做其它的一些功能了。

2、避免CPU空轉,應用程式經常會涉及到RPC,資料庫訪問,磁碟IO等操作,這些操作的速度比CPU慢很多,而在等待這些響應時,CPU卻不能去處理新的請求,導致這種單執行緒的應用程式效能很差。

3、提升效率,一個程序要獨立擁有4GB的虛擬地址空間,而多個執行緒可以共享同一地址空間,執行緒的切換比程序的切換要快得多。

二、  如何使用多執行緒技術進行程式設計?

下面給出個多執行緒程式,一個最簡單的模擬售票系統,程式碼如下:

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

void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;

int main()
{
	pthread_t id1,id2;
	int error;

	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}

void *ticketsell1(void *arg)
{
	while(1)
	{
		if(tickets > 0)
		{
//			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
		}
		else
		{
			break;
		}
	}
	return (void *)0;
}

void *ticketsell2(void *arg)
{
	while(1)
	{
		if(tickets > 0)
		{
//			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
		}
		else
		{
			break;
		}
	}

	return (void *)0;
}

執行結果如下:

[email protected]:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse2 sells ticket:19
ticketse2 sells ticket:18
ticketse2 sells ticket:17
ticketse2 sells ticket:16
ticketse2 sells ticket:15
ticketse2 sells ticket:14
ticketse2 sells ticket:13
ticketse2 sells ticket:12
ticketse2 sells ticket:11
ticketse2 sells ticket:10
ticketse2 sells ticket:9
ticketse2 sells ticket:8
ticketse2 sells ticket:7
ticketse2 sells ticket:6
ticketse2 sells ticket:4
ticketse2 sells ticket:3
ticketse2 sells ticket:2
ticketse2 sells ticket:1
ticketse1 sells ticket:5

看到結果,我們發現時能正常賣票的,一部分連續是sel2,另一部分是ticketsel1;

此時,其實存在一個隱含的問題,就是執行緒間的切換,在單CPU系統中,CPU是有時間片時間,時間片到了,就要執行其它的執行緒,假設thread1執行到if裡面,但在printf執行前發生了執行緒切換,那麼會發生什麼呢?我們在這裡用usleep函式(放開程式中的usleep註釋行)進行強制模擬切換;

我們看看結果:

[email protected]:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread
[email protected]:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse1 sells ticket:19
ticketse2 sells ticket:18
ticketse1 sells ticket:17
ticketse2 sells ticket:16
ticketse1 sells ticket:15
ticketse2 sells ticket:14
ticketse1 sells ticket:13
ticketse2 sells ticket:12
ticketse1 sells ticket:11
ticketse2 sells ticket:10
ticketse1 sells ticket:9
ticketse2 sells ticket:8
ticketse1 sells ticket:7
ticketse2 sells ticket:6
ticketse1 sells ticket:5
ticketse2 sells ticket:4
ticketse1 sells ticket:3
ticketse1 sells ticket:2
ticketse2 sells ticket:1
ticketse1 sells ticket:0
[email protected]:~/qiang/mthread$ 

執行程式發現竟然有0號票被賣出了,這顯然是錯誤的!當thread1的if裡面發生執行緒切換時,thread2得到執行,把最後一張票賣了,此時thread1恢復執行,結果賣出了0號票,這裡我們需要的是火車票的票數資料對於所有執行緒而言是同步的,所以就要用到執行緒同步技術了。

三、  使用多執行緒的同步與互斥

1、多執行緒的同步方式有很多種,例如互斥鎖,條件變數,訊號量,讀寫鎖。先看看互斥鎖如何解決多執行緒之間的同步問題。程式用互斥鎖後如下:

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

void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
pthread_mutex_t mutex;

int main()
{
	pthread_t id1,id2;
	pthread_mutex_init(&mutex, NULL);//
	int error;

	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}

void *ticketsell1(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//給互斥量上鎖
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			pthread_mutex_unlock(&mutex);//給互斥量解鎖
			
		}
		else
		{
			pthread_mutex_unlock(&mutex);//給互斥量解鎖
			break;
		}
		pthread_yield();//執行緒排程函式,使每個執行緒都有執行機會
	}
	return (void *)0;
}

void *ticketsell2(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//給互斥量上鎖
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			pthread_mutex_unlock(&mutex);//給互斥量解鎖
		}
		else
		{
			pthread_mutex_unlock(&mutex);//給互斥量解鎖
			break;
		}
		pthread_yield();//執行緒排程函式,是兩個執行緒都有執行機會
	}

	return (void *)0;
}

執行結果如下:

[email protected]:~/qiang/mthread$ vi mthread1.c
[email protected]:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread
[email protected]:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse1 sells ticket:19
ticketse2 sells ticket:18
ticketse1 sells ticket:17
ticketse2 sells ticket:16
ticketse1 sells ticket:15
ticketse2 sells ticket:14
ticketse1 sells ticket:13
ticketse2 sells ticket:12
ticketse1 sells ticket:11
ticketse2 sells ticket:10
ticketse1 sells ticket:9
ticketse2 sells ticket:8
ticketse1 sells ticket:7
ticketse2 sells ticket:6
ticketse1 sells ticket:5
ticketse2 sells ticket:4
ticketse1 sells ticket:3
ticketse2 sells ticket:2
ticketse1 sells ticket:1


2、再看看用訊號量來解決多執行緒的同步問題,程式程式碼如下:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
sem_t mutex,full;

int main()
{
	pthread_t id1,id2;
	int error;
	int ret;

	ret = sem_init(&mutex, 0 ,1);//初始化mutex訊號量為1
	ret += sem_init(&full, 0 ,0);//初始化full訊號量為0

	if(ret != 0)
	{
		printf("sem_init fails!\n");
	}

	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}

void *ticketsell1(void *arg)
{
	while(1)
	{
		sem_wait(&mutex);//mutex訊號量進行P操作
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			sem_post(&full);//full訊號量進行V操作
		}
		else
		{
			sem_post(&full);//full訊號量進行V操作
			break;
		}
	}
	return (void *)0;
}

void *ticketsell2(void *arg)
{
	while(1)
	{
		sem_wait(&full);//full訊號量進行P操作
		if(tickets > 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			sem_post(&mutex);//mutex訊號量進行V操作
		}
		else
		{
			sem_post(&mutex);//mutex訊號量進行V操作
			break;
		}
	}

	return (void *)0;
}

執行結果:

[email protected]:~/qiang/mthread$ vi mthread1.c
[email protected]:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread
[email protected]:~/qiang/mthread$ ./mthread1 
ticketse1 sells ticket:20
ticketse2 sells ticket:19
ticketse1 sells ticket:18
ticketse2 sells ticket:17
ticketse1 sells ticket:16
ticketse2 sells ticket:15
ticketse1 sells ticket:14
ticketse2 sells ticket:13
ticketse1 sells ticket:12
ticketse2 sells ticket:11
ticketse1 sells ticket:10
ticketse2 sells ticket:9
ticketse1 sells ticket:8
ticketse2 sells ticket:7
ticketse1 sells ticket:6
ticketse2 sells ticket:5
ticketse1 sells ticket:4
ticketse2 sells ticket:3
ticketse1 sells ticket:2
ticketse2 sells ticket:1
[email protected]:~/qiang/mthread$ 

上面的sem_init函式用來初始化兩個訊號量的初始化值,這裡一個設為1,一個設為0,sem_wait類似於P操作,讓訊號量減1,如果小於結果小於0,執行緒阻塞,否則執行緒繼續執行,sem_post類似於V操作,提升訊號量的值,加1,通過這兩個訊號量之間的互相“救對方”,就可以實現這兩個執行緒的同步執行。

我們編譯執行以上程式,發現兩個售票點交替賣票,兩個純程依次得到機會執行,並且不會有0號票賣出,實現了同步。

3、我們再用條件變數來解決同步問題,一般條件變數需要結合互斥量一起使用,程式碼如下

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
pthread_mutex_t mutex;
pthread_cond_t  qready = PTHREAD_COND_INITIALIZER;//靜態初始化條件變數;
		
int main()
{
	pthread_t id1,id2;
	pthread_mutex_init(&mutex, NULL);
	int error;

	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}

	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}

void *ticketsell1(void *arg)
{
	pthread_mutex_lock(&mutex);
	while(tickets > 0)
	{
		if(tickets%2 == 1)
		{
			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
			pthread_cond_signal(&qready);//條件改變,傳送訊號,通知ticketse2
		}
		else
		{
			pthread_cond_wait(&qready,&mutex);//解開Mutex,並等待qready改變
		}
		pthread_mutex_unlock(&mutex);//給互斥量解鎖
	}
	return (void *)0;
}

void *ticketsell2(void *arg)
{
	pthread_mutex_lock(&mutex);
	while(tickets > 0)
	{
		if(tickets%2 == 0)
		{
			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
			pthread_cond_signal(&qready);//條件改變,傳送訊號,通知ticketse1
		}
		else
		{
			pthread_cond_wait(&qready,&mutex);//解開mutex,並等待qready改變
		}
		pthread_mutex_unlock(&mutex);//給互斥量解鎖
	}

	return (void *)0;
}

執行結果如下:

[email protected]:~/qiang/mthread$ vi mthread1.c
[email protected]:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread
[email protected]:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse1 sells ticket:19
ticketse2 sells ticket:18
ticketse1 sells ticket:17
ticketse2 sells ticket:16
ticketse1 sells ticket:15
ticketse2 sells ticket:14
ticketse1 sells ticket:13
ticketse2 sells ticket:12
ticketse1 sells ticket:11
ticketse2 sells ticket:10
ticketse1 sells ticket:9
ticketse2 sells ticket:8
ticketse1 sells ticket:7
ticketse2 sells ticket:6
ticketse1 sells ticket:5
ticketse2 sells ticket:4
ticketse1 sells ticket:3
ticketse2 sells ticket:2
ticketse1 sells ticket:1
[email protected]:~/qiang/mthread$ 


  條件變數通過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件變數發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變數被用來進行執行緒間的同步.

函式pthread_cond_wait使執行緒阻塞在一個條件變數上,而函式pthread_cond_signal是用來釋放被阻塞在條件變數上的一個執行緒。但是要注意的是,條件變數只是起到阻塞和喚醒執行緒的作用,具體的判斷條件還需使用者給出,我這裡給出的是tickets是否是偶數這個條件。