談一談C++中的多線程(上)

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

何爲進程?何爲線程?兩者有何區別?

何爲併發?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++中的多線程。

單線程版:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
 
int GetSum(vector<int>::iterator first,vector<int>::iterator last)
{
    return accumulate(first,last,0);//調用C++標準庫算法
}
 
int main()
{
    vector<int> largeArrays;
    for(int i=0;i<100000000;i++)
    {
        if(i%2==0)
           largeArrays.push_back(i);
        else
            largeArrays.push_back(-1*i);
    }
    int res = GetSum(largeArrays.begin(),largeArrays.end());
    return 0;
}

多線程版:

#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
using namespace std;
 
//線程要做的事情就寫在這個線程函數中
void GetSumT(vector<int>::iterator first,vector<int>::iterator last,int &result)
{
    result = accumulate(first,last,0); //調用C++標準庫算法
}
 
int main() //主線程
{
    int result1,result2,result3,result4,result5;
    vector<int> largeArrays;
    for(int i=0;i<100000000;i++)
    {
        if(i%2==0)
            largeArrays.push_back(i);
        else
            largeArrays.push_back(-1*i);
    }
    thread first(GetSumT,largeArrays.begin(),
        largeArrays.begin()+20000000,std::ref(result1)); //子線程1
    thread second(GetSumT,largeArrays.begin()+20000000,
        largeArrays.begin()+40000000,std::ref(result2)); //子線程2
    thread third(GetSumT,largeArrays.begin()+40000000,
        largeArrays.begin()+60000000,std::ref(result3)); //子線程3
    thread fouth(GetSumT,largeArrays.begin()+60000000,
        largeArrays.begin()+80000000,std::ref(result4)); //子線程4
    thread fifth(GetSumT,largeArrays.begin()+80000000,
        largeArrays.end(),std::ref(result5)); //子線程5
 
    first.join(); //主線程要等待子線程執行完畢
    second.join();
    third.join();
    fouth.join();
    fifth.join();
 
    int resultSum = result1+result2+result3+result4+result5; //彙總各個子線程的結果
 
    return 0;
}

C++11中引入了多線程技術,通過thread線程類對象來管理線程,只需要#include 即可。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,因此需要從線程中返回值往往採用傳遞引用的方法。我們講,傳遞引用相當於擴充了變量的作用域。

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

PS:因篇幅限制,本篇文章先寫到這裏,餘下內容會在下一篇文章中講解。謝謝讀者朋友們的閱讀,歡迎提出寶貴意見,期待與您交流!

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