Linux --- 多線程

一.線程的基本概念

1.線程概念
進程是對運行時程序的封裝,是系統進行資源調度和分配的的基本單位,實現了操作系統的併發;

線程是進程的子任務,是CPU調度和分派的基本單位,用於保證程序的實時性,實現進程內部的併發;線程是操作系統可識別的最小執行和調度單位。每個線程都獨自佔用一個虛擬處理器:獨自的寄存器組,指令計數器和處理器狀態。每個線程完成不同的任務,但是共享同一地址空間(也就是同樣的動態內存,映射文件,目標代碼等等),打開的文件隊列和其他內核資源。

2.線程的優點

  • (1)創建一個新線程的代價要比創建一個新進程小得多

  • (2)與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多

  • (3)線程佔用的資源要比進程少很多

  • (4)能充分利用多處理器的可並行數量

  • (5)在等待慢速I/O操作結束的同時,程序可執行其他的計算任務

  • (6)計算密集型應用,爲了能在多處理器系統上運行,將計算分解到多個線程中實現

  • (7)I/O密集型應用,爲了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。

3.線程的缺點

  • (1)性能損失:一個很少被外部事件阻塞的計算密集型線程往往無法與其他線程共享一個處理器,如果計算密集型線程的數量比可用的處理器多,那麼就會造成較大的性能損失,這裏的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。

  • (2)健壯性降低:編寫多進程需要更全面深入的考慮,在一個多線程程序裏,因事件分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的。

  • (3)缺乏訪問控制:進程是訪問控制的基本粒度,在一個線程中調用某些os函數會對整個進程造成影響。

  • (4)編程難度提高:編寫與調試一個多線程程序比單線程程序困難得多。

4.線程異常

  • (1)單個線程如果出現除零,野指針問題導致線程崩潰,進程也會隨之崩潰。
  • (2)線程是進程得執行分支,線程出異常,就類似於進程出異常,進而觸發信號機制,終止進程,進程終止,該進程內得所以線程也就會隨之退出。

5.線程用途

  • (1)合理得使用多線程,能提高CPU密集型程序得執行效率
  • (2)合理得使用多線程,能提高IO密集型程序的用戶體驗。

6.進程與線程之間的區別

  • (1)一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程依賴於進程而存在。

  • (2)進程在執行過程中擁有獨立的內存單元,而多個線程共享進程的內存。(資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。)

  • (3)進程是資源分配的最小單位,線程是CPU調度的最小單位;

  • (4)系統開銷: 由於在創建或撤消進程時,系統都要爲之分配或回收資源,如內存空間、I/o設備等。因此,操作系統所付出的開銷將顯著地大於在創建或撤消線程時的開銷。類似地,在進行進程切換時,涉及到整個當前進程CPU環境的保存以及新被調度運行的進程的CPU環境的設置。而線程切換隻須保存和設置少量寄存器的內容,並不涉及存儲器管理方面的操作。可見,進程切換的開銷也遠大於線程切換的開銷。

  • (5)通信:由於同一進程中的多個線程具有相同的地址空間,致使它們之間的同步和通信的實現,也變得比較容易。進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。在有的系統中,線程的切換、同步和通信都無須操作系統內核的干預

  • (6)進程編程調試簡單可靠性高,但是創建銷燬開銷大;線程正相反,開銷小,切換速度快,但是編程調試相對複雜。

  • (7)進程間不會相互影響 ;線程一個線程掛掉將導致整個進程掛掉

  • (8)進程適應於多核、多機分佈;線程適用於多核

二.線程控制

1.創建線程

//功能:創建一個新的線程 
//原型    
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void*), void *arg); 
//參數    
	//thread:返回線程ID    
	//attr:設置線程的屬性,
	//attr爲NULL表示使用默認屬性    
	//start_routine:是個函數地址,線程啓動後要執行的函數    
	//arg:傳給線程啓動函數的參數 
//返回值:成功返回0;失敗返回錯誤碼
//創建一個線程的基本操作。
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 
  7 
  8 using namespace std;
  9 
 10 void *thread_routine(void* arg)//調用函數指針
 11 {
 12     string str = (char*) arg;
 13     while(1)
 14     {
 15         cout << str << " run "  << "pid : " << getpid() << endl;
 16         sleep(1);
 17     }
 18 }
 19 int main()//兩個線程的pid相同
 20 {
 21     pthread_t tid;
 22     pthread_create(&tid, NULL, thread_routine, (void*) "thread 1");//創建線程
 23     while(1)
 24     {
 25         cout << "main thread run " << " pid : " << getpid() << endl;
 26         sleep(2);
 27     }
 28     return 0;
 29 }

在這裏插入圖片描述
對於線程而言:
LMP:線程ID,也就是gettid()的返回值。
NLWP:線程組內線程的個數。

2.線程等待(默認是阻塞式的)
線程join只關心線程結果是否正確,默認線程是成功的(沒有出異常的)。

//功能:等待線程結束 
//原型    
	int pthread_join(pthread_t thread, void **value_ptr); 
//參數    
	//thread:線程ID    
	//value_ptr:它指向一個指針,後者指向線程的返回值 
//返回值:成功返回0;失敗返回錯誤碼

3.線程終止
如果需要只終止某個線程而不終止整個進程,可以有三種方法:

  1. 從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit。
  2. 線程可以調用pthread_ exit終止自己。
  3. 一個線程可以調用pthread_ cancel終止同一進程中的另一個線程。
//功能:線程終止 
//原型    
	void pthread_exit(void *value_ptr); 
//參數    
	value_ptr:value_ptr不要指向一個局部變量。 
//返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)
//需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,
//不能在線程函數 的棧上分配,因爲當其它線程得到這個返回指針時線程函數已經退出了
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 void *thread_routine(void* arg)
 11 {
 12     while(1)
 13     {
 14         printf("Hello I am a new thread,my name is : %s\n", (char*)arg);
 15         sleep(1);
 16         break;
 17     }
 18     //return (void*)11;
 19     //exit(3);
 20     pthread_exit((void*)3);//線程退出
 21 }
 22 int main()
 23 {
 24     pthread_t tid;
 25     pthread_create(&tid, NULL, thread_routine, (void *)"thread 1");//創建線程
 26     printf("Hello I am main thread: %p\n", tid );
 27 
 28     void * ret;
 29     pthread_join(tid, &ret);//線程等待,jion的第二個參數是void* 的返回值
 30     printf("ret : %d\n", ret);//打印退出碼
 31     return 0;//main函數return代表進程退出。
 32 }

4.線程取消

//功能:取消一個執行中的線程 
//原型    
	int pthread_cancel(pthread_t thread); 
//參數    
	thread:線程ID 
//返回值:成功返回0;失敗返回錯誤碼

5.獲取線程自身的id

  • (1)pthread_ create函數會產生一個線程ID,存放在第一個參數指向的地址中。該線程ID和前面說的線程ID不是 一回事。
  • (2)前面講的線程ID屬於進程調度的範疇。因爲線程是輕量級進程,是操作系統調度器的最小單位,所以需要 一個數值來唯一表示該線程。
  • (3)pthread_ create函數第一個參數指向一個虛擬內存單元,該內存單元的地址即爲新創建線程的線程ID,屬於 NPTL線程庫的範疇。
  • (4)線程庫的後續操作,就是根據該線程ID來操作線程的。 線程庫NPTL提供了pthread_ self函數,可以獲得線程自身的ID:
pthread_t pthread_self(void);

pthread_t到底是什麼類型?取決於實現,對於目前實現的NPTL實現而言,pthread_t類型的線程ID,本質上就是一個進程地址空間的地址。

6.線程分離

  • (1)默認情況下,新創建的線程是joinable的,線程退出後,需要對其進行pthread_join操作,否則無法釋放資 源,從而造成系統泄漏。
  • (2) 如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程 資源。
int pthread_detach(pthread_t thread);

新線程可以取消主線程,但是主線程不會退出,主線程就會變成殭屍進程,沒有人回收。
整個進程的退出交給bash。
當不關心線程的運行狀態或者是線程的資源回收交給操作系統的話,就叫做線程分離。

三.線程互斥

1.概念

  • (1)臨界資源:多線程執行流共享的資源就叫做臨界資源。
  • (2)臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區。
  • (3)互斥:任何時候,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用。
  • (4)原子性:不會被任何調度機制打斷的操作,該操作只有兩個狀態,要麼完成,要麼未完成
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 int tickets = 100;
 11 
 12 void* GetTicket(void* args)
 13 {
 14     while(1)
 15     {
 16         if(tickets > 0)
 17         {
 18             usleep(10000);
 19             printf("get a ticket no . is : %d\n",tickets--);
 20         }
 21         else
 22         {
 23             printf("%s ... quit\n", (char*)args);
 24             break;
 25         }
 26     }
 27 }
 28 int main()
 29 {
 30     pthread_t tid1, tid2, tid3, tid4;
 31     pthread_create(&tid1, NULL, GetTicket, (void *)"thread 1");
 32     pthread_create(&tid2, NULL, GetTicket, (void *)"thread 2");
 33     pthread_create(&tid3, NULL, GetTicket, (void *)"thread 3");
 34     pthread_create(&tid4, NULL, GetTicket, (void *)"thread 4");
 35 
 36     pthread_join(tid1, NULL);
 37     pthread_join(tid2, NULL);
 38     pthread_join(tid3, NULL);
 39     pthread_join(tid4, NULL);
 40     return 0;
 41 }

對於上面的代碼編程,最後的結果不是正確的,爲什麼會這樣?主要有以下幾點:
(1)if語句判斷條件爲真以後,代碼可以併發的切換到其他線程。
(2)usleep這個模擬漫長業務的過程,在這個漫長的業務過程中,可能有很多個線程會進入該代碼段
(3)–ticket操作本身就不是一個原子操作。

上面操作不是原子性,而是對應三個操作
(1)load:將共享變量ticket從內存中加載到寄存器中。
(2)updata:更新寄存器中的值,執行-1操作。
(3)store:將新值,從寄存器寫回共享變量ticket的內存地址。

要解決以上問題,需要做到三點:
(1)代碼必須有互斥行爲,當代碼進入臨界區執行時,不允許其他線程進入臨界區。
(2)如果多個線程同時要求執行臨界區的代碼,並且臨界區沒有線程在執行,那麼只能允許一個線程進入該臨界區。
(3)如果線程不在臨界區中執行,那麼該線程不能組織其他線程進入臨界區。
要做到三點,本質上就是需要一把鎖,Linux上提供的這把鎖叫互斥量。

2.初始化互斥量的兩種方法

//(1)靜態分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

//(2)動態分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);    
參數:        
	//mutex:要初始化的互斥量        
	//attr:NULL

3.互斥量的銷燬
使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷燬
不要銷燬一個已經加鎖的互斥量 已經銷燬的互斥量,
要確保後面不會有線程再嘗試加鎖。

int pthread_mutex_destroy(pthread_mutex_t *mutex)

4.互斥量加鎖和解鎖

int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
//返回值:成功返回0,失敗返回錯誤號

調用pthread_lock時,可能會遇到以下情況:
(1)互斥量處於未鎖狀態,那麼函數會將互斥量鎖定,同時返回成功。
(2)發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那麼pthread_lock會陷入阻塞(執行流被掛起),等待互斥量解鎖。

//將上面的代碼修改如下,加上鎖,就保證了原子性。
  1 #include<iostream>
  2 #include<pthread.h>
  3 #include<string>
  4 #include<sys/types.h>
  5 #include<unistd.h>
  6 #include<cstdio>
  7 #include<stdlib.h>
  8 using namespace std;
  9 
 10 int tickets = 100;
 11 pthread_mutex_t lock;//設置一個全局的鎖
 12 
 13 void* GetTicket(void* args)
 14 {
 15     while(1)
 16     {
 17         pthread_mutex_lock(&lock);//加鎖
 18         if(tickets > 0)
 19         {
 20             usleep(10000);
 21             printf("get a ticket no . is : %d\n",tickets--);
 22             pthread_mutex_unlock(&lock);//解鎖
 23         }
 24         else
 25         {
 26             printf("%s ... quit\n", (char*)args);
 27             pthread_mutex_unlock(&lock);//解鎖
 28             break;
 29         }
 30     }
 31 }
 32 int main()
 33 {
 34     pthread_t tid1, tid2, tid3, tid4;
 35     pthread_mutex_init(&lock,NULL);//初始化互斥量(鎖)
 36     pthread_create(&tid1, NULL, GetTicket, (void *)"thread 1");
 37     pthread_create(&tid2, NULL, GetTicket, (void *)"thread 2");
 38     pthread_create(&tid3, NULL, GetTicket, (void *)"thread 3");
 39     pthread_create(&tid4, NULL, GetTicket, (void *)"thread 4");
 40 
 41     pthread_join(tid1, NULL);
 42     pthread_join(tid2, NULL);
 43     pthread_join(tid3, NULL);
 44     pthread_join(tid4, NULL);
 45		pthread_mutex_destroy(&lock);//銷燬鎖。
 46     return 0;
 47 }

cpu內部有若干寄存器:通用寄存器,狀態寄存器,指令寄存器。
上下文信息:線程在運行期間運行到什麼地方,運行的狀態等。

四.可重入和線程安全

1.概念:

  • (1)線程安全:多個線程併發同一段代碼時,不會出現不同的結果,常見對全局變量或者靜態變量進行操作,並且在沒有鎖保護的情況下,會出現該問題。
  • (2)重入:同一個函數被不同的執行流調用,當前一個進程還沒有執行完,就有其他的執行流再次進入,我們將其稱爲重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱爲可重入函數,否則,是不可重入函數。

2.常見的線程不安全的情況

  • (1)不保護共享變量的函數
  • (2)函數狀態隨着被調用,狀態發生變化的函數
  • (3)返回指向靜態變量指針的函數
  • (4)調用線程不安全函數的函數。

3.常見的線程安全的情況

  • (1)每個線程對全局變量或者靜態變量只有讀取的權限,而沒有寫入的權限,一般來說這些線程是安全的
  • (2)類或者接口對於線程來說都是原子操作
  • (3)多個線程之間的切換不會導致該接口的執行結果存在二義性

4.常見的不可重入的函數

  • (1)不使用全局變量或靜態變量
  • (2)不使用用malloc或者new開闢出的空間
  • (3)不調用不可重入函數
  • (3)不返回靜態或全局數據,所有數據都有函數的調用者提供 。
  • (4)使用本地數據,或者通過製作全局數據的本地拷貝來保護全局數據。

5.常見可重入的情況

  • (1)不使用全局變量或靜態變量
  • (2)不使用用malloc或者new開闢出的空間
  • (3)不調用不可重入函數
  • (4)不返回靜態或全局數據,所有數據都有函數的調用者提供
  • (5)使用本地數據,或者通過製作全局數據的本地拷貝來保護全局數據

6.可重入與線程安全聯繫

  • (1)函數是可重入的,那就是線程安全的
  • (2)函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題
  • (3)如果一個函數中有全局變量,那麼這個函數既不是線程安全也不是可重入的。

7.可重入與線程安全區別

  • (1)可重入函數是線程安全函數的一種
  • (2)線程安全不一定是可重入的,而可重入函數則一定是線程安全的。
  • (3)如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生 死鎖,因此是不可重入的

五.死鎖

1.概念:
死鎖是指在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資 源而處於的一種永久等待狀態。

死鎖四個必要條件

  • (1)互斥條件:一個資源每次只能被一個執行流使用
  • (2)請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
  • (3)不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪
  • (4)循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關係

避免死鎖

  • (1)破壞死鎖的四個必要條件
  • (2)加鎖順序一致
  • (3)避免鎖未釋放的場景
  • (4)資源一次性分配

避免死鎖算法

  • 死鎖檢測算法(瞭解)
  • 銀行家算法(瞭解)

六.Linux線程同步

1.條件變量:

  • (1)當一個線程互斥地訪問某個變量時,它可能發現在其他線程改變狀態之前,它什麼都做不了。
  • (2)例如,一個線程訪問隊列時,發現隊列爲空,它只能等待,直到其他線程將一個節點添加到隊列中,這種情況都需要用到條件變量。

2.同步概念與競態條件
同步:在保證數據安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免飢餓問 題,叫做同步 。
競態條件:因爲時序問題,而導致程序異常,我們稱之爲競態條件。在線程場景下,這種問題也不難理解。

3.條件變量函數
(1)初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 
//參數:    
	//cond:要初始化的條件變量    
	//attr:NULL

(2)銷燬

int pthread_cond_destroy(pthread_cond_t *cond)

(3)等待條件滿足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);    
//參數:        
	//cond:要在這個條件變量上等待        
	//mutex:互斥量,

(4)喚醒等待

 int pthread_cond_broadcast(pthread_cond_t *cond);   
 int pthread_cond_signal(pthread_cond_t *cond);

七.生產者和消費者模型

1.概念:生產者消費者模型就是通過一個容器來解決生產者和消費者的強耦合問題,生產者和消費者彼此之間不直接通訊,而是通過阻塞隊列來進行通訊,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力,這個阻塞隊列就是用來給生產者和消費者解耦的。

能夠進行生產和消費的只能是進程或者線程。
交易場所是內存空間,貨物就是數據。

(2)三種關係(兩類角色和一個交易場所)
生產者和生產者(互斥)
消費者和消費者(互斥)
生產者和消費者(同步與互斥)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章