【學(xué)習(xí)筆記】C++并發(fā)與多線程筆記一:基本概念和用法

一俏站、基本概念

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)如下:

  1. 線程啟動速度更快蟀伸,更輕量級蚀同;
  2. 系統(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)備工作:

  1. 添加頭文件 thread.h刊棕。
  2. 添加 std 命名空間犹芹。
  3. 定義一個(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!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喻旷,一起剝皮案震驚了整個(gè)濱河市生逸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌且预,老刑警劉巖槽袄,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锋谐,居然都是意外死亡遍尺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門涮拗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乾戏,“玉大人,你說我怎么就攤上這事三热」脑瘢” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵就漾,是天一觀的道長呐能。 經(jīng)常有香客問我,道長抑堡,這世上最難降的妖魔是什么摆出? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮夷野,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荣倾。我一直安慰自己悯搔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布舌仍。 她就那樣靜靜地躺著妒貌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪铸豁。 梳的紋絲不亂的頭發(fā)上灌曙,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音节芥,去河邊找鬼在刺。 笑死逆害,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蚣驼。 我是一名探鬼主播魄幕,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼颖杏!你這毒婦竟也來了纯陨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤留储,失蹤者是張志新(化名)和其女友劉穎翼抠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體获讳,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阴颖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赔嚎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膘盖。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖尤误,靈堂內(nèi)的尸體忽然破棺而出侠畔,到底是詐尸還是另有隱情,我是刑警寧澤损晤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布软棺,位于F島的核電站,受9級特大地震影響尤勋,放射性物質(zhì)發(fā)生泄漏喘落。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一最冰、第九天 我趴在偏房一處隱蔽的房頂上張望瘦棋。 院中可真熱鬧,春花似錦暖哨、人聲如沸赌朋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沛慢。三九已至,卻和暖如春达布,著一層夾襖步出監(jiān)牢的瞬間团甲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工黍聂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留躺苦,地道東北人身腻。 一個(gè)月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像圾另,于是被迫代替她去往敵國和親霸株。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353