一個簡單的Linux多線程例子 帶你洞悉互斥量 信號量 條件變量編程


      希望此文能給初學多線程編程的朋友帶來幫助,也希望牛人多多指出錯誤。

      另外感謝以下鏈接的作者給予,給我的學習帶來了很大幫助

      http://blog.csdn.net/locape/article/details/6040383

      http://www.cnblogs.com/liuweijian/archive/2009/12/30/1635888.html

一、什么是多線程?

      當我自己提出這個問題的時候,我還是很老實的拿着操作系統的書,按着上面的話敲下“為了減少進程切換和創建開銷,提高執行效率和節省資源,我們引入了線程的概念,與進程相比較,線程是CPU調度的一個基本單位。”

      形象點的舉個例子說:一個WEB服務器可以同時接收來自不同用戶的網頁訪問請求,顯然服務器處理這些網頁請求都是通過並發進行的否則將會造成用戶等待時間長或者響應效率低等問題。如果在服務器中使用進程的辦法來處理來自不同網頁訪問請求的話,我們可以用創建父進程以及多個子進程的方法,然而這樣會花費很大的系統開銷和占用較多的資源,因此這樣會較大的限制了訪問服務器的用戶數量。

      使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。我們知道,在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小於啟動一個進程所花費的空間

      使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改

二、互斥鎖

      正如上面所說的,如果兩個線程同時對一塊內存進行讀寫或者對向同一個文件寫數據,那么結果是難以設想的。正因為如此,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應於一個可稱為" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。

 例如int *a  int *b  分別指向兩塊內存,上面的值分別初始化為(200, 100) 線程A執行這樣的一個操作:將*a的值減少50,*b的值增加50.

 線程B執行:打印出(a 跟 b 指向的內存的值的和)。

      如果串行運行:A: *a -= 50; *b += 50;  B: printf("%d\n", *a + *b);  

      如果並發執行,則有可能會出現一下調度:*a -= 50; printf("%d\n", *a + *b); *b += 50;

      因此我們可以引入互斥量,在對共享數據讀寫時進行鎖操作,實現對內存的訪問以互斥的形式進行。

      

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

int a = 200;
int b = 100;
pthread_mutex_t lock;

void* ThreadA(void*)
{
pthread_mutex_lock(&lock);          //鎖
a -= 50;
sleep(5);                                      //執行到一半 使用sleep 放棄cpu調度
b += 50;
pthread_mutex_unlock(&lock);
}

void* ThreadB(void*)
{
sleep(1);                            //放棄CPU調度 目的先讓A線程運行。
pthread_mutex_lock(&lock);
printf("%d\n", a + b);
pthread_mutex_unlock(&lock);
}

int main()
{
pthread_t tida, tidb;
pthread_mutex_init(&lock, NULL);
pthread_create(&tida, NULL, ThreadA, NULL);
pthread_create(&tidb, NULL, ThreadB, NULL);
pthread_join(tida, NULL);
pthread_join(tidb, NULL);
return 1;
}

    以上輸出為300  去掉鎖操作  輸出為250。

三、信號量      

 

  作用域 上鎖時
信號量 進程間或線程間(linux僅線程間) 只要信號量的value大於0,其他線程就可以sem_wait成功,成功后信號量的value減一。若value值不大於0,則sem_wait阻塞,直到sem_post釋放后value值加一
互斥鎖 線程間 只要被鎖住,其他任何線程都不可以訪問被保護的資源 
成功后否則就阻塞

       信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加信號量。只有當信號量值大於0時,才能使用公共資源,使用后,函數sem_wait()減少信號量。

       信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以后再進行自己下面的步驟,這個任務並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。

       可能這樣說大家對信號量的概念還是很模糊,舉個例子,現在有個圖書館,其能容納100人,現在有兩個線程A、B,A線程執行:往圖書管理進入一個人,B線程:從圖書館出來一個人。那么為了使得線程A在圖書館滿人的時候進入等待,而不是繼續往圖書館里進人,使得B線程在圖書館沒人的時候等待人進入,我們可以引入信號量:IN  OUT  分別初始化為100和0。

        那么A則可以被表示為A:P(IN) //剩余容量減少一 如果容量為0 則等待                     B:P(OUT) //人數減少1  如果人數為0則阻塞等待

                                        .........   //登記進入圖書館的人信息                                  .............  //記錄 離開圖書館的人信息   (隨便一系列操作。)

                                        V(OUT)//增加信號量OUT 表示人數+1                           V(IN)   //增加圖書館剩余容量+1

         通過這樣我們就實現了線程的同步。

         sem_init 用語初始化一個信號量其原型:int sem_init(sem_t *sem, int pshared, unsigned int value);

     sem_init() 初始化一個定位在 sem 的匿名信號量。value 參數指定信號量的初始值。 pshared 參數指明信號量是由進程內線程共享,還是由進程之間共享。如果 pshared 的值為 0,那么信號量將被進程內的線程共享,並且應該放置在所有線程都可見的地址上

         上面的P V操作相當於sem_wait ()   sempost()

          sem_wait函數也是一個原子操作,它的作用是從信號量的值減去一個“1”,但它永遠會先等待該信號量為一個非零值才開始做減法。也就是說,如果你對一個值為2的信號量調用sem_wait(),線程將會繼續執行,這信號量的值將減到1。如果對一個值為0的信號量調用sem_wait(),這個函數就 會地等待直到有其它線程增加了這個值使它不再是0為止。如果有兩個線程都在sem_wait()中等待同一個信號量變成非零值,那么當它被第三個線程增加 一個“1”時,等待線程中只有一個能夠對信號量做減法並繼續執行,另一個還將處於等待狀態。

         sem_post的作用很簡單,就是使信號量增加1

三、條件變量

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

      假設有共享的資源sum,與之相關聯的mutex 是lock_s.假設每個線程對sum的操作很簡單的,與sum的狀態無關,比如只是sum++.那么只用mutex足夠了.程序員只要確保每個線程操作前,取得lock,然后sum++,再unlock即可.每個線程的代碼將像這樣

      

add() 
{  
    pthread_mutex_lock(lock_s); 
    sum++; 
    pthread_mutex_unlock(lock_s); 
}

  如果操作比較復雜,假設線程t0,t1,t2的操作是sum++,而線程t3則是在sum到達100的時候,打印出一條信息,並對sum清零. 這種情況下,如果只用mutex, 則t3需要一個循環,每個循環里先取得lock_s,然后檢查sum的狀態,如果sum>=100,則打印並清零,然后unlock.如果sum& amp; amp; amp; lt;100,則unlock,並sleep()本線程合適的一段時間. 

     這個時候,t0,t1,t2的代碼不變,t3的代碼如下:

print()  
{  
       while  
      {  
		pthread_mutex_lock(lock_s);  
		if(sum>=100)  
		{  
			printf(“sum reach 100!”);  
			pthread_mutex_unlock(lock_s);  
		}  
		else
		{  
			pthread_mutex_unlock(lock_s);  
			my_thread_sleep(100);  
			return  ;  
		}  
	}  
}

這種辦法有兩個問題 
1) sum在大多數情況下不會到達100,那么對t3的代碼來說,大多數情況下,走的是else分支,只是lock和unlock,然后sleep().這浪費了CPU處理時間. 
2) 為了節省CPU處理時間,t3會在探測到sum沒到達100的時候sleep()一段時間.這樣卻又帶來另外一個問題,亦即t3響應速度下降.可能在sum到達200的時候,t3才醒過來. 
3) 這樣,程序員在設置sleep()時間的時候陷入兩難境地,設置得太短了節省不了資源,太長了又降低響應速度.真是難辦啊!

這個時候,condition variable內褲外穿,從天而降,拯救了焦頭爛額的你. (抄襲的哈哈~)

  你首先定義一個condition variable. 
  pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER; 
  t0,t1,t2的代碼只要后面加兩行,像這樣

add() 
{ 
	pthread_mutex_lock(lock_s); 
	sum++; 
	pthread_mutex_unlock(lock_s); 
	if(sum>=100) 
		pthread_cond_signal(&cond_sum_ready); 
} 

T3線程的print

print { pthread_mutex_lock(lock_s); while(sum<100) pthread_cond_wait(&cond_sum_ready, &lock_s); printf(“sum is over 100!”); sum=0; pthread_mutex_unlock(lock_s); return; }

注意兩點:
1) 在thread_cond_wait()之前,必須先lock相關聯的mutex, 因為假如目標條件未滿足,pthread_cond_wait()實際上會unlock該mutex, 然后block,在目標條件滿足后再重新lock該mutex, 然后返回.
2) 為什么是while(sum<100),而不是if(sum<100) ?這是因為在pthread_cond_signal()和pthread_cond_wait()返回之間,有時間差,假設在這個時間差內,還有另外一個線程t4又把sum減少到100以下了,那么t3在pthread_cond_wait()返回之后,顯然應該再檢查一遍sum的大小.
這就是用 while的用意

四、實戰:     

下面是一個例子:

 

//一個多線程例子:三個線程  0 1 往水池中加水。 線程2放水。  如果水池滿 則阻塞進水線程  等待

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

pthread_mutex_t Poll_Work;       //互斥鎖
sem_t Poll_IN;                   //水池容量信號量                   
sem_t Poll_OUT;                  //當前水量信號量

void* thread0(void *param)            //0 1線程往水池中加水。
{
	while(1){
		int rv = 0;
		for(int i = 0; i < 5; ++ i)
			while((rv = sem_wait(&Poll_IN)) != 0 && (errno == EINTR))          //水量增加5  循環使得容量信號量減少5
				;
		pthread_mutex_lock(&Poll_Work);               //加鎖  線程互斥
		(*(int*)param) += 5;
		printf("Thread0: %d\n", *(int*)param);
		for(int i = 0; i < 5; ++ i)
			sem_post( &Poll_OUT);                    //增加當前水量信號量
		pthread_mutex_unlock(&Poll_Work);             //解鎖。
		
		sleep(1);
	}
	return NULL;
}

void* thread1(void *param)
{
	while(1){
		int rv = 0;
		for(int i = 0; i < 4; ++ i)
			while((rv = sem_wait(&Poll_IN)) != 0 && (errno == EINTR) )    
				;
		pthread_mutex_lock(&Poll_Work);
		(*(int*)param) += 4;
		printf("Thread1: %d\n", *(int*)param);
		for(int i = 0; i < 4; ++ i)
			sem_post( &Poll_OUT);
		pthread_mutex_unlock(&Poll_Work);
		
		sleep(1);
	}
	return NULL;
}

void* thread2(void *param)                   //此線程用於將水從池中倒出。。
{
	while(1){
		int rv = 0;
		for(int i = 0; i < 3; ++ i)
			while((rv = sem_wait(&Poll_OUT)) != 0 && (errno == EINTR))      //減少當前水量
				printf("xx");
		pthread_mutex_lock(&Poll_Work);
		(*(int*)param) -= 3; 
		printf("Thread2: %d\n", *(int*)param);
		for(int i = 0; i < 3; ++ i)
			sem_post(&Poll_IN);                                     //水池容量信號量增加
		pthread_mutex_unlock(&Poll_Work);
		sleep(1);
	}
	return NULL;
}


int main()
{
	int sum = 0;   //水深  滿為100米 初始化 池里沒有水
	int i;

	pthread_mutex_init(&Poll_Work, NULL);
	sem_init(&Poll_IN, 0, 100);
	sem_init(&Poll_OUT, 0, sum);
	pthread_t ths[4];
	pthread_create(&ths[0], NULL,  thread0, (void*)&sum);
	pthread_create(&ths[1], NULL,  thread1, (void*)&sum);
	pthread_create(&ths[2], NULL,  thread2, (void*)&sum);
	for(i = 0; i < 3; ++ i){
		pthread_join(ths[i], NULL);
	}
}

 

  到此,希望能給大家帶來幫助~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM