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

一、基本概念

1.1 并發(fā)、進(jìn)程、線程

1.1.1 并發(fā)

并發(fā)是指兩個(gè)或者更多的任務(wù)(獨(dú)立的活動(dòng))同時(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)程默默啟動(dòng)起來了。

線程:是操作系統(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í)行通路。

/* 主線程啟動(dòng)時(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ú)立的堆??臻g,線程之間的切換要保存很多中間狀態(tài),會(huì)耗費(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ā)

  • 比如賬號(hào)服務(wù)器一個(gè)進(jìn)程,游戲服務(wù)器一個(gè)進(jìn)程,二者之間存在通信;
  • 服務(wù)器進(jìn)程之間存在通信,比如同一電腦下的管道,文件,消息隊(duì)列,共享內(nèi)存等;不同電腦下的 socket 通信等。

1.2.2 多線程并發(fā)

線程:感覺像是輕量級(jí)的進(jìn)程。每個(gè)進(jìn)程有自己獨(dú)立的運(yùn)行路徑,但一個(gè)進(jìn)程中的所有線程共享地址空間(共享內(nèi)存),全局變量、全局內(nèi)存、全局引用都可以在線程之間傳遞,所以多線程開銷遠(yuǎn)遠(yuǎn)小于多進(jìn)程,但這也會(huì)引入一個(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ù)量過多反而會(huì)導(dǎo)致效率降低。

1.2.3 總結(jié)

和進(jìn)程比,線程的優(yōu)點(diǎn)如下:

  1. 線程啟動(dòng)速度更快,更輕量級(jí);
  2. 系統(tǒng)資源開銷更少,執(zhí)行速度更快,比如共享內(nèi)存這種通信方式比任何其他的通信方式都快。

缺點(diǎn):使用有一定難度,要小心處理數(shù)據(jù)的一致性問題。

1.3 C++11新標(biāo)準(zhǔn)線程庫

以往的多線程代碼通常調(diào)用系統(tǒng)平臺(tái)提供的接口實(shí)現(xiàn),不能跨平臺(tái)運(yùn)行,比如在 Windows 平臺(tái)創(chuàng)建線程使用 CreateThread() 接口,但 Linux 平臺(tái)則使用 pthread_create() 接口。

當(dāng)然,使用 POSIX thread(pthread)庫也可以實(shí)現(xiàn)跨平臺(tái),但需要在不同的平臺(tái)上進(jìn)行配置,用起來也不是特別方便。

從 C++ 11 標(biāo)準(zhǔn)開始,C++語言本身增加了對(duì)多線程的支持,意味著增強(qiáng)了可移植性(跨平臺(tái)),減少了開發(fā)人員的工作量。

二、C++線程基本用法

2.1 線程運(yùn)行的開始和結(jié)束

  • 程序運(yùn)行起來,生成一個(gè)進(jìn)程,該進(jìn)程所屬的主線程開始自動(dòng)運(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í)行完,也會(huì)被強(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è)線程對(duì)象:

int main()
{
    /* 創(chuàng)建一個(gè)thread對(duì)象, 并以myThreadEntry()作為線程入口函數(shù) */
    /* 其中myThreadEntry是可執(zhí)行對(duì)象(函數(shù)指針) */
    thread newThread(myThreadEntry);
    return 0;
}

備注:線程類(thread類)參數(shù)是一個(gè)可調(diào)用對(duì)象。一組可執(zhí)行的語句稱為可調(diào)用對(duì)象,C++中的可調(diào)用對(duì)象可以是函數(shù)、函數(shù)指針、lambda表達(dá)式、bind創(chuàng)建的對(duì)象或者重載了函數(shù)調(diào)用運(yùn)算符的類對(duì)象。

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對(duì)象就會(huì)失去與主線的關(guān)聯(lián)(變成孤兒),此時(shí)這個(gè)子線程就會(huì)駐留在后臺(tái)運(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);
    /* 分離主線程與子線程 */
    /* 子線程駐留在后臺(tái)運(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(),就不能對(duì)子線程使用 join() 了。detach() 使我們完全失去了對(duì)子線程的控制,因此不建議這樣用,還是調(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)用對(duì)象

#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類對(duì)象 */
    thread newThread(ta); /* ta:可調(diào)用對(duì)象 */
    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對(duì)象時(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類對(duì)象 */
    thread newThread(ta); /* ta:可調(diào)用對(duì)象 */
    newThread.detach();   /* 分離主線程和子線程 */
    return 0;
}

問題一:調(diào)用detach()分離了主線程與子線程后,它們將分別獨(dú)立運(yùn)行,當(dāng)主線程結(jié)束后,,局部變量 my_i 將被回收釋放,此時(shí)子線程中的 m_i 引用了 my_i,這個(gè)值就是個(gè)無效的值,無法預(yù)料會(huì)有什么結(jié)果。

問題二:在主線程中,ta也是局部變量,主線程運(yùn)行完畢,按常理來說,ta對(duì)象也被釋放了,為什么調(diào)用detach()后子線程還能正常運(yùn)行呢?

首先,主線程運(yùn)行完畢后,ta對(duì)象肯定是不在了,但是這個(gè)對(duì)象不在了也沒關(guān)系,因?yàn)檫@個(gè)對(duì)象實(shí)際上是被復(fù)制到線程中去的,所以執(zhí)行完主線程后,ta對(duì)象會(huì)被銷毀,但是所復(fù)制的ta對(duì)象依舊存在。

只要TA類對(duì)象里沒有引用、沒有指針,那么就不會(huì)產(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 對(duì)象。

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)用對(duì)象 */
    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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容