1. 程式人生 > 其它 >淺談C++中的多執行緒

淺談C++中的多執行緒

C++多執行緒併發程式設計視訊:https://pan.baidu.com/s/1uDf2aMmyRLPgKQaUvSx67A

提取碼:g421

本篇文章圍繞以下幾個問題展開:

何為程序?何為執行緒?兩者有何區別?
何為併發?C++中如何解決併發問題?C++中多執行緒的語言實現?
同步互斥原理以及多程序和多執行緒中實現同步互斥的兩種方法
Qt中的多執行緒應用
引入
傳統的C++(C++98)中並沒有引入執行緒這個概念。linux和unix作業系統的設計採用的是多程序,程序間的通訊十分方便,同時程序之間互相有著獨立的空間,不會汙染其他程序的資料,天然的隔離性給程式的穩定性帶來了很大的保障。而執行緒一直都不是linux和unix推崇的技術,甚至有傳言說linus本人就非常不喜歡執行緒的概念。隨著C++市場份額被Java、Python等語言所蠶食,為了使得C++更符合現代語言的特性,在C++11中引入了多執行緒與併發技術。

一.何為程序?何為執行緒?兩者有何區別?
1.何為程序?
程序是一個應用程式被作業系統拉起來載入到記憶體之後從開始執行到執行結束的這樣一個過程。簡單來說,程序是程式(應用程式,可執行檔案)的一次執行。程序通常由程式、資料和程序控制塊(PCB)組成。比如雙擊開啟一個桌面應用軟體就是開啟了一個程序。

傳統的程序有兩個基本屬性:可擁有資源的獨立單位;可獨立排程和分配的基本單位。對於這句話我的理解是:程序可以獲取作業系統分配的資源,如記憶體等;程序可以參與作業系統的排程,參與CPU的競爭,得到分配的時間片,獲得處理機(CPU)執行。

程序在建立、撤銷和切換中,系統必須為之付出較大的時空開銷,因此在系統中開啟的程序數不宜過多。比如你同時開啟十幾個應用軟體試試,電腦肯定會卡死的。於是緊接著就引入了執行緒的概念。

2.何為執行緒?
執行緒是程序中的一個實體,是被系統獨立分配和排程的基本單位。也有說,執行緒是CPU可執行排程的最小單位。也就是說,程序本身並不能獲取CPU時間,只有它的執行緒才可以。

引入執行緒之後,將傳統程序的兩個基本屬性分開了,執行緒作為排程和分配的基本單位,程序作為獨立分配資源的單位。我對這句話的理解是:執行緒參與作業系統的排程,參與CPU的競爭,得到分配的時間片,獲得處理機(CPU)執行。而程序負責獲取作業系統分配的資源,如記憶體。

執行緒基本上不擁有資源,只擁有一點執行中必不可少的資源,它可與同屬一個程序的其他執行緒共享程序所擁有的全部資源。

執行緒具有許多傳統程序所具有的特性,故稱為“輕量型程序”。同一個程序中的多個執行緒可以併發執行。

3.程序和執行緒的區別?
其實根據程序和執行緒的定義已經能區分開它們了。

執行緒分為使用者級執行緒和核心支援執行緒兩類,使用者級執行緒不依賴於核心,該類執行緒的建立、撤銷和切換都不利用系統呼叫來實現;核心支援執行緒依賴於核心,即無論是在使用者程序中的執行緒,還是在系統中的執行緒,它們的建立、撤銷和切換都利用系統呼叫來實現。

但是,與執行緒不同的是,無論是系統程序還是使用者程序,在進行切換時,都要依賴於核心中的程序排程。因此,無論是什麼程序都是與核心有關的,是在核心支援下程序切換的。儘管執行緒和程序表面上看起來相似,但是他們在本質上是不同的。

根據作業系統中的知識,程序至少必須有一個執行緒,通常將此執行緒稱為主執行緒。

程序要獨立的佔用系統資源(如記憶體),而同一程序的執行緒之間是共享資源的。程序本身並不能獲取CPU時間,只有它的執行緒才可以。

4.其他
程序在建立、撤銷和切換過程中,系統的時空開銷非常大。使用者可以通過建立執行緒來完成任務,以減少程式併發執行時付出的時空開銷。例如可以在一個程序中設定多個執行緒,當一個執行緒受阻時,第二個執行緒可以繼續執行,當第二個執行緒受阻時,第三個執行緒可以繼續執行......。這樣,對於擁有資源的基本單位(程序),不用頻繁的切換,進一步提高了系統中各種程式的併發程度。

在一個應用程式(程序)中同時執行多個小的部分,這就是多執行緒。這小小的部分雖然共享一樣的資料,但是卻做著不同的任務。

二.何為併發?C++中如何解決併發問題?C++中多執行緒的語言實現?
1.何為併發?
1.1.併發
在同一個時間裡CPU同時執行兩條或多條命令,這就是所謂的併發。

1.2.偽併發
偽併發是一種看似併發的假象。我們知道,每個應用程式是由若干條指令組成的。在現代計算機中,不可能一次只跑一個應用程式的命令,CPU會以極快的速度不停的切換不同應用程式的命令,而讓我們看起來感覺計算機在同時執行很多個應用程式。比如,一邊聽歌,一邊聊天,還能同時打遊戲,我們誤以為這是併發,其實只是一種偽併發的假象。

主要,以前的計算機都是單核CPU,就不太可能實現真正的併發,只能是不同的執行緒佔用不同的時間片,而CPU在各個執行緒之間來回快速的切換。

偽併發的模型大致如下:

整個框代表一個CPU的執行,T1和T2代表兩個不同的執行緒,在執行期間,不同的執行緒分別佔用不同的時間片,然後由作業系統負責排程執行不同的執行緒。但是很明顯,由於記憶體、暫存器等等都是有限的,所以在執行下一個執行緒的時候不得不把上一個執行緒的一些資料先儲存起來,這樣下一次執行該執行緒的時候才能繼續正確的執行。

這樣多執行緒的好處就是更大的利用CPU的空閒時間,而缺點就是要付出一些其他的代價,所以多執行緒是否一定要單執行緒快呢?答案是否定的。這個道理就像,如果有3個程式設計師同時編寫一個專案,不可避免需要相互的交流,如果這個交流的時間遠遠大於編碼的時間,那麼拋開程式碼質量來說,可能還不如一個程式猿來的快。

理想的併發模型如下:

可以看出,這是真正的併發,真正實現了時間效率上的提高。因為每一個框代表一個CPU的執行,所以真正實現併發的物理基礎的多核CPU。

1.3.併發的物理基礎
慢慢的,發展出了多核CPU,這樣就為實現真併發提供了物理基礎。但這僅僅是硬體層面提供了併發的機會,還需要得到語言的支援。像C++11之前缺乏對於多執行緒的支援,所寫的併發程式也僅僅是偽併發。

也就是說,併發的實現必須首先得到硬體層面的支援,不過現在的計算機已經是多核CPU了,我們對於併發的研究更多的是語言層面和軟體層面了。

2.C++中如何解決併發問題?
顯然通過多程序來實現併發是不可靠的,C++中採用多執行緒實現併發。

執行緒算是一個底層的,傳統的併發實現方法。C++11中除了提供thread庫,還提供了一套更加好用的封裝好了的併發程式設計方法。

C++中更高階的併發方法:(此內容因本人暫未理解,暫時擱置,待理解之時會前來更新,請讀者朋友諒解)

3.C++中多執行緒的語言實現?
這裡以一個典型的示例——求和函式來講解C++中的多執行緒。

單執行緒版:

 1 #include <iostream>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5  
 6 int GetSum(vector<int>::iterator first,vector<int>::iterator last)
 7 {
 8     return accumulate(first,last,0);//呼叫C++標準庫演算法
 9 }
10  
11 int main()
12 {
13     vector<int> largeArrays;
14     for(int i=0;i<100000000;i++)
15     {
16         if(i%2==0)
17            largeArrays.push_back(i);
18         else
19             largeArrays.push_back(-1*i);
20     }
21     int res = GetSum(largeArrays.begin(),largeArrays.end());
22     return 0;
23 }

多執行緒版:

 1 #include <iostream>
 2 #include <vector>
 3 #include <algorithm>
 4 #include <thread>
 5 using namespace std;
 6  
 7 //執行緒要做的事情就寫在這個執行緒函式中
 8 void GetSumT(vector<int>::iterator first,vector<int>::iterator last,int &result)
 9 {
10     result = accumulate(first,last,0); //呼叫C++標準庫演算法
11 }
12  
13 int main() //主執行緒
14 {
15     int result1,result2,result3,result4,result5;
16     vector<int> largeArrays;
17     for(int i=0;i<100000000;i++)
18     {
19         if(i%2==0)
20             largeArrays.push_back(i);
21         else
22             largeArrays.push_back(-1*i);
23     }
24     thread first(GetSumT,largeArrays.begin(),
25         largeArrays.begin()+20000000,std::ref(result1)); //子執行緒1
26     thread second(GetSumT,largeArrays.begin()+20000000,
27         largeArrays.begin()+40000000,std::ref(result2)); //子執行緒2
28     thread third(GetSumT,largeArrays.begin()+40000000,
29         largeArrays.begin()+60000000,std::ref(result3)); //子執行緒3
30     thread fouth(GetSumT,largeArrays.begin()+60000000,
31         largeArrays.begin()+80000000,std::ref(result4)); //子執行緒4
32     thread fifth(GetSumT,largeArrays.begin()+80000000,
33         largeArrays.end(),std::ref(result5)); //子執行緒5
34  
35     first.join(); //主執行緒要等待子執行緒執行完畢
36     second.join();
37     third.join();
38     fouth.join();
39     fifth.join();
40  
41     int resultSum = result1+result2+result3+result4+result5; //彙總各個子執行緒的結果
42  
43     return 0;
44 }

C++11中引入了多執行緒技術,通過thread執行緒類物件來管理執行緒,只需要#include <thread>即可。thread類物件的建立意味著一個執行緒的開始。

thread first(執行緒函式名,引數1,引數2,......);每個執行緒有一個執行緒函式,執行緒要做的事情就寫線上程函式中。

根據作業系統上的知識,一個程序至少要有一個執行緒,在C++中可以認為main函式就是這個至少的執行緒,我們稱之為主執行緒。而在建立thread物件的時候,就是在這個執行緒之外建立了一個獨立的子執行緒。這裡的獨立是真正的獨立,只要建立了這個子執行緒並且開始運行了,主執行緒就完全和它沒有關係了,不知道CPU會什麼時候排程它執行,什麼時候結束執行,一切都是獨立,自由而未知的。

因此下面要講兩個必要的函式:join()和detach()

如:thread first(GetSumT,largeArrays.begin(),largeArrays.begin()+20000000,std::ref(result1)); first.join();

這意味著主執行緒和子執行緒之間是同步的關係,即主執行緒要等待子執行緒執行完畢才會繼續向下執行,join()是一個阻塞函式。

而first.detach(),當然上面示例中並沒有應用到,則表示主執行緒不用等待子執行緒執行完畢,兩者脫離關係,完全放飛自我。這個一般用在守護執行緒上:有時候我們需要建立一個暗中觀察的執行緒,默默查詢程式的某種狀態,這種的稱為守護執行緒。這種執行緒會在主執行緒銷燬之後自動銷燬。

C++中一個標準執行緒函式只能返回void,因此需要從執行緒中返回值往往採用傳遞引用的方法。我們講,傳遞引用相當於擴充了變數的作用域。

我們為什麼需要多執行緒,因為我們希望能夠把一個任務分解成很多小的部分,各個小部分能夠同時執行,而不是隻能順序的執行,以達到節省時間的目的。對於求和,把所有資料一起相加和分段求和再相加沒什麼區別。