一俏站、基本概念
1.1 并發(fā)研乒、進(jìn)程止吁、線程
1.1.1 并發(fā)
并發(fā)是指兩個(gè)或者更多的任務(wù)(獨(dú)立的活動)同時(shí)發(fā)生(進(jìn)行):一個(gè)程序同時(shí)執(zhí)行多個(gè)獨(dú)立的任務(wù)什乙。
以往的計(jì)算機(jī)通常是單核cpu挽封,某一個(gè)時(shí)刻只能執(zhí)行一個(gè)任務(wù),此時(shí)由操作系統(tǒng)調(diào)度實(shí)現(xiàn)并發(fā)臣镣,即每秒鐘進(jìn)行多次所謂的“任務(wù)切換”辅愿,造成并發(fā)的假象,這種切換(上下文切換)存在時(shí)間開銷忆某,比如操作系統(tǒng)需要保存切換時(shí)的各種狀態(tài)点待、執(zhí)行進(jìn)度等信息,并且切換回來時(shí)好需要復(fù)原這些信息褒繁。
隨著硬件的發(fā)展亦鳞,出現(xiàn)了多處理器計(jì)算機(jī)项乒,用于服務(wù)器和高新能計(jì)算領(lǐng)域节腐,比如一塊芯片上有個(gè)核心(cpu):雙核、4核秽褒、8核等等坝冕,它們能實(shí)現(xiàn)真正的并行執(zhí)行多個(gè)任務(wù)(硬件并發(fā))徒探。
使用并發(fā)的目的:可以同時(shí)干多個(gè)事,提高性能喂窟。
1.1.2 進(jìn)程
在了解進(jìn)程之前测暗,首先需要知道什么叫程序,程序是指令磨澡、數(shù)據(jù)及其組織形式的描述碗啄,而進(jìn)程就是程序的實(shí)體。
簡單理解稳摄,一個(gè)可執(zhí)行程序運(yùn)行起來了稚字,就叫創(chuàng)建了一個(gè)進(jìn)程。進(jìn)程就是運(yùn)行起來的可執(zhí)行程序。
1.1.3 線程
每個(gè)進(jìn)程胆描,都有唯一的一個(gè)主線程瘫想。執(zhí)行可執(zhí)行程序產(chǎn)生一個(gè)進(jìn)程后,這個(gè)主線程就隨著這個(gè)進(jìn)程默默啟動起來了昌讲。
線程:是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中短绸,是進(jìn)程中的實(shí)際運(yùn)作單位车吹。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程醋闭,每條線程并行執(zhí)行不同的任務(wù)礼搁。
簡單理解,線程就是用來執(zhí)行代碼的目尖,可以理解為一條代碼的執(zhí)行通路。
/* 主線程啟動時(shí)執(zhí)行main()函數(shù) */
int main()
{
/* 各種代碼 */
return 0;
}
/*
* 主線程執(zhí)行完main()函數(shù)return后扎运,表示整個(gè)進(jìn)程允許完畢瑟曲,
* 此時(shí)主線程結(jié)束運(yùn)行,整個(gè)進(jìn)程也結(jié)束運(yùn)行豪治。
*/
主線程由系統(tǒng)創(chuàng)建洞拨,除了它之外,可以通過寫代碼來創(chuàng)建其他線程负拟,其他線程走的是別的道路烦衣,甚至去不同的地方。
每創(chuàng)建一個(gè)新線程掩浙,就可以在同一時(shí)刻花吟,多干一個(gè)不同的事(多走一條不同的代碼執(zhí)行路徑)。
程序中同時(shí)運(yùn)行多個(gè)線程時(shí)厨姚,即實(shí)現(xiàn)了并發(fā)衅澈,但線程并不是越多越好,每個(gè)線程谬墙,都需要一個(gè)獨(dú)立的堆椊癫迹空間,線程之間的切換要保存很多中間狀態(tài)拭抬,會耗費(fèi)本該屬于程序運(yùn)行的時(shí)間部默。
1.2 并發(fā)的實(shí)現(xiàn)方法
實(shí)現(xiàn)并發(fā)的手段:
- 通過多個(gè)進(jìn)程實(shí)現(xiàn)并發(fā)
- 在一個(gè)進(jìn)程中,創(chuàng)建多個(gè)線程實(shí)現(xiàn)并發(fā)
1.2.1 多進(jìn)程并發(fā)
- 比如賬號服務(wù)器一個(gè)進(jìn)程造虎,游戲服務(wù)器一個(gè)進(jìn)程傅蹂,二者之間存在通信;
- 服務(wù)器進(jìn)程之間存在通信累奈,比如同一電腦下的管道贬派,文件急但,消息隊(duì)列,共享內(nèi)存等搞乏;不同電腦下的 socket 通信等波桩。
1.2.2 多線程并發(fā)
線程:感覺像是輕量級的進(jìn)程。每個(gè)進(jìn)程有自己獨(dú)立的運(yùn)行路徑请敦,但一個(gè)進(jìn)程中的所有線程共享地址空間(共享內(nèi)存)镐躲,全局變量、全局內(nèi)存侍筛、全局引用都可以在線程之間傳遞萤皂,所以多線程開銷遠(yuǎn)遠(yuǎn)小于多進(jìn)程,但這也會引入一個(gè)新問題:數(shù)據(jù)一致性問題匣椰。
多進(jìn)程并發(fā)和多線程并發(fā)可以混合使用裆熙,但通常優(yōu)先考慮多線程技術(shù)。
備注:使用多線程并發(fā)時(shí)禽笑,創(chuàng)建線程的數(shù)量最大不建議超過 200-300個(gè)入录,至于多少合適,需要根據(jù)實(shí)際項(xiàng)目情況進(jìn)行調(diào)整佳镜,有時(shí)線程數(shù)量過多反而會導(dǎo)致效率降低僚稿。
1.2.3 總結(jié)
和進(jìn)程比,線程的優(yōu)點(diǎn)如下:
- 線程啟動速度更快蟀伸,更輕量級蚀同;
- 系統(tǒng)資源開銷更少,執(zhí)行速度更快啊掏,比如共享內(nèi)存這種通信方式比任何其他的通信方式都快蠢络。
缺點(diǎn):使用有一定難度,要小心處理數(shù)據(jù)的一致性問題迟蜜。
1.3 C++11新標(biāo)準(zhǔn)線程庫
以往的多線程代碼通常調(diào)用系統(tǒng)平臺提供的接口實(shí)現(xiàn)谢肾,不能跨平臺運(yùn)行,比如在 Windows 平臺創(chuàng)建線程使用 CreateThread() 接口小泉,但 Linux 平臺則使用 pthread_create() 接口芦疏。
當(dāng)然,使用 POSIX thread(pthread)庫也可以實(shí)現(xiàn)跨平臺微姊,但需要在不同的平臺上進(jìn)行配置酸茴,用起來也不是特別方便。
從 C++ 11 標(biāo)準(zhǔn)開始兢交,C++語言本身增加了對多線程的支持薪捍,意味著增強(qiáng)了可移植性(跨平臺),減少了開發(fā)人員的工作量。
二酪穿、C++線程基本用法
2.1 線程運(yùn)行的開始和結(jié)束
- 程序運(yùn)行起來凳干,生成一個(gè)進(jìn)程,該進(jìn)程所屬的主線程開始自動運(yùn)行被济;當(dāng)主線程從main()函數(shù)返回救赐,則整個(gè)進(jìn)程執(zhí)行完畢。
- 主線程從main()開始執(zhí)行只磷,那么我們自己創(chuàng)建的線程经磅,也需要從一個(gè)函數(shù)開始運(yùn)行(初始函數(shù)),一旦這個(gè)函數(shù)運(yùn)行完畢钮追,線程也結(jié)束運(yùn)行预厌。
- 整個(gè)進(jìn)程是否執(zhí)行完畢的標(biāo)志是主線程是否執(zhí)行完,如果主線程執(zhí)行完畢就代表整個(gè)進(jìn)程執(zhí)行完畢了元媚,此時(shí)如果其他子線程還沒有執(zhí)行完轧叽,也會被強(qiáng)行終止。
準(zhǔn)備工作:
- 添加頭文件 thread.h刊棕。
- 添加 std 命名空間犹芹。
- 定義一個(gè)線程入口函數(shù)。
#include <iostream>
#include <thread>
using namespace std;
/* 線程入口函數(shù) */
void myThreadEntry()
{
cout << "My thread start!!!" << endl;
/* 線程執(zhí)行代碼 */
cout << "My thread end!!!" << endl;
}
2.1.1 thread()
創(chuàng)建一個(gè)線程對象:
int main()
{
/* 創(chuàng)建一個(gè)thread對象鞠绰, 并以myThreadEntry()作為線程入口函數(shù) */
/* 其中myThreadEntry是可執(zhí)行對象(函數(shù)指針) */
thread newThread(myThreadEntry);
return 0;
}
備注:線程類(thread類)參數(shù)是一個(gè)可調(diào)用對象。一組可執(zhí)行的語句稱為可調(diào)用對象飒焦,C++中的可調(diào)用對象可以是函數(shù)蜈膨、函數(shù)指針、lambda表達(dá)式牺荠、bind創(chuàng)建的對象或者重載了函數(shù)調(diào)用運(yùn)算符的類對象翁巍。
2.1.2 join()
等待線程執(zhí)行執(zhí)行完畢:
int main()
{
thread newThread(myThreadEntry);
/* 阻塞主線程,等待子線程執(zhí)行完畢 */
/* 當(dāng)myThreadEntry執(zhí)行完畢休雌,join()就執(zhí)行完畢灶壶,主線程繼續(xù)往下執(zhí)行 */
newThread.join();
cout << "Hello World!" << endl;
return 0;
}
輸出結(jié)果:
My thread start!!!
My thread end!!!
Hello World!
2.1.3 detach()
傳統(tǒng)多線程程序主線程必須等待子線程執(zhí)行完畢后,自己才能最終退出杈曲,但 C++11 中提供了 detach() 接口驰凛,它用于分離主線程和子線程,即主線程不必等待子線程運(yùn)行結(jié)束担扑,主線程是否退出不影響子線程的運(yùn)行恰响。
一旦 detach() 之后,與主線程關(guān)聯(lián)的thread對象就會失去與主線的關(guān)聯(lián)(變成孤兒)涌献,此時(shí)這個(gè)子線程就會駐留在后臺運(yùn)行胚宦,這個(gè)子線程就相當(dāng)于被C++運(yùn)行時(shí)庫接管了,當(dāng)這個(gè)子線程運(yùn)行完成后,由運(yùn)行時(shí)庫負(fù)責(zé)清理該線程相關(guān)的資源(守護(hù)線程)枢劝。
int main()
{
thread newThread(myThreadEntry);
/* 分離主線程與子線程 */
/* 子線程駐留在后臺運(yùn)行井联,被C++運(yùn)行時(shí)庫接管 */
newThread.detach();
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
return 0;
}
輸出結(jié)果:
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
My thread start!!!
Hello World!
My thread end!!!
Hello World!
Hello World!
備注:主線程和子線程分離后,輸出結(jié)果交替打印您旁,且每次運(yùn)行的輸出結(jié)果都不一樣烙常。
一旦調(diào)用 detach(),就不能對子線程使用 join() 了被冒。detach() 使我們完全失去了對子線程的控制军掂,因此不建議這樣用,還是調(diào)用 join() 正常等待子線程退出更加安全可靠譜昨悼。
2.1.4 joinable()
判斷是否可以成功使用 join() 或者 detach()蝗锥。
int main()
{
thread newThread(myThreadEntry);
/* 如果返回true,證明可以調(diào)用join()或者detach() */
if (newThread.joinable()) {
cout << "1.joinable() == true" << endl;
} else {
cout << "1.joinable() == false" << endl;
}
newThread.detach();
/* 如果返回false率触,證明調(diào)用過join()或detach()终议,二者都不能再調(diào)用了 */
if (newThread.joinable()) {
cout << "2.joinable() == true" << endl;
} else {
cout << "2.joinable() == false" << endl;
}
return 0;
}
輸出結(jié)果:
1.joinable() == true
2.joinable() == false
2.2 其他創(chuàng)建線程的方法
2.2.1 用類
定義一個(gè)TA類型,并重載一個(gè)無參數(shù)的()操作葱蝗,讓其變成一個(gè)可調(diào)用對象:
#include <iostream>
#include <thread>
using namespace std;
/* 定義TA類 */
class TA {
public:
void operator()() { /* 重載()操作(無參數(shù)) */
cout << "My thread start!!!" << endl;
/* 線程執(zhí)行代碼 */
cout << "My thread end!!!" << endl;
}
};
int main()
{
TA ta; /* 聲明TA類對象 */
thread newThread(ta); /* ta:可調(diào)用對象 */
newThread.join(); /* 等待newThrad執(zhí)行完畢 */
cout << "Hello World!" << endl;
return 0;
}
輸出結(jié)果:
My thread start!!!
My thread end!!!
Hello World!
一個(gè)使用 detach() 的坑:
#include <iostream>
#include <thread>
using namespace std;
class TA {
public:
int &m_i; /* 定義一個(gè)應(yīng)用 */
TA(int &i) :m_i(i) {} /* 創(chuàng)建TA對象時(shí)需傳入一個(gè)引用值 */
void operator()() {
/* 主線程結(jié)束后穴张,局部變量my_i值已被釋放 */
/* 而m_i是my_i的引用,此時(shí)將產(chǎn)生不可預(yù)料的后果 */
cout << "1.m_i = " << m_i << endl;
cout << "2.m_i = " << m_i << endl;
cout << "3.m_i = " << m_i << endl;
cout << "4.m_i = " << m_i << endl;
cout << "5.m_i = " << m_i << endl;
cout << "6.m_i = " << m_i << endl;
}
};
int main()
{
int my_i = 6;
TA ta(my_i); /* 聲明TA類對象 */
thread newThread(ta); /* ta:可調(diào)用對象 */
newThread.detach(); /* 分離主線程和子線程 */
return 0;
}
問題一:調(diào)用detach()分離了主線程與子線程后两曼,它們將分別獨(dú)立運(yùn)行皂甘,當(dāng)主線程結(jié)束后,,局部變量 my_i 將被回收釋放悼凑,此時(shí)子線程中的 m_i 引用了 my_i偿枕,這個(gè)值就是個(gè)無效的值,無法預(yù)料會有什么結(jié)果户辫。
問題二:在主線程中渐夸,ta也是局部變量,主線程運(yùn)行完畢渔欢,按常理來說墓塌,ta對象也被釋放了,為什么調(diào)用detach()后子線程還能正常運(yùn)行呢奥额?
首先苫幢,主線程運(yùn)行完畢后,ta對象肯定是不在了垫挨,但是這個(gè)對象不在了也沒關(guān)系态坦,因?yàn)檫@個(gè)對象實(shí)際上是被復(fù)制到線程中去的,所以執(zhí)行完主線程后棒拂,ta對象會被銷毀伞梯,但是所復(fù)制的ta對象依舊存在玫氢。
只要TA類對象里沒有引用、沒有指針谜诫,那么就不會產(chǎn)生問題漾峡。
驗(yàn)證代碼:
#include <iostream>
#include <thread>
using namespace std;
class TA {
public:
int &m_i;
TA(int &i) :m_i(i) {
cout << "TA()構(gòu)造函數(shù)被執(zhí)行" << endl;
}
TA(const TA &ta) :m_i(ta.m_i){
cout << "TA()拷貝構(gòu)造函數(shù)被執(zhí)行" << endl;
}
~TA(){
cout << "~TA()析構(gòu)函數(shù)被執(zhí)行" << endl;
}
void operator()() {
cout << "1.m_i = " << m_i << endl;
cout << "2.m_i = " << m_i << endl;
cout << "3.m_i = " << m_i << endl;
cout << "4.m_i = " << m_i << endl;
cout << "5.m_i = " << m_i << endl;
cout << "6.m_i = " << m_i << endl;
}
};
int main()
{
int my_i = 6;
TA ta(my_i); /* 調(diào)用TA構(gòu)造函數(shù) */
thread newThread(ta); /* 調(diào)用TA拷貝構(gòu)造函數(shù) */
newThread.detach(); /* 分離主線程和子線程 */
cout << "Hello World!" << endl;
return 0;
}
輸出結(jié)果:
TA()構(gòu)造函數(shù)被執(zhí)行
TA()拷貝構(gòu)造函數(shù)被執(zhí)行
Hello World!
~TA()析構(gòu)函數(shù)被執(zhí)行
將 detach() 換成 join() 之后的輸出結(jié)果:
TA()構(gòu)造函數(shù)被執(zhí)行
TA()拷貝構(gòu)造函數(shù)被執(zhí)行
1.m_i = 6
2.m_i = 6
3.m_i = 6
4.m_i = 6
5.m_i = 6
6.m_i = 6
~TA()析構(gòu)函數(shù)被執(zhí)行
Hello World!
~TA()析構(gòu)函數(shù)被執(zhí)行
可以看出 join() 中釋放了深度拷貝到子線程中的 ta 對象。
2.2.2 用lambda表達(dá)式
使用lambda表達(dá)式創(chuàng)建線程的示例代碼如下:
#include <iostream>
#include <thread>
using namespace std;
int main()
{
auto myLamThread = [] {
cout << "My thread start!!!" << endl;
/* 線程執(zhí)行代碼 */
cout << "My thread end!!!" << endl;
};
thread newThread(myLamThread); /* myLamThread:可調(diào)用對象 */
newThread.join(); /* 等待線程結(jié)束 */
cout << "Hello World!" << endl;
return 0;
}
輸出結(jié)果:
My thread start!!!
My thread end!!!
Hello World!