C++多線程并發(fā)基礎(chǔ)

什么是C++多線程并發(fā)宝与?

線程:線程是操作系統(tǒng)能夠進(jìn)行CPU調(diào)度的最小單位梗醇,它被包含在進(jìn)程之中鳄抒,一個(gè)進(jìn)程可包含單個(gè)或者多個(gè)線程闯捎∫祝可以用多個(gè)線程去完成一個(gè)任務(wù),也可以用多個(gè)進(jìn)程去完成一個(gè)任務(wù)瓤鼻,它們的本質(zhì)都相當(dāng)于多個(gè)人去合伙完成一件事秉版。

多線程并發(fā):多線程是實(shí)現(xiàn)并發(fā)(雙核的真正并行或者單核機(jī)器的任務(wù)切換都叫并發(fā))的一種手段,多線程并發(fā)即多個(gè)線程同時(shí)執(zhí)行,一般而言娱仔,多線程并發(fā)就是把一個(gè)任務(wù)拆分為多個(gè)子任務(wù)沐飘,然后交由不同線程處理不同子任務(wù),使得這多個(gè)子任務(wù)同時(shí)執(zhí)行。

C++多線程并發(fā): C++98標(biāo)準(zhǔn)中并沒有線程庫的存在,而在C++11中才提供了多線程的標(biāo)準(zhǔn)庫,提供了管理線程牲迫、保護(hù)共享數(shù)據(jù)耐朴、線程間同步操作、原子操作等類盹憎,筛峭。(簡單情況下)實(shí)現(xiàn)C++多線程并發(fā)程序的思路如下:將任務(wù)的不同功能交由多個(gè)函數(shù)分別實(shí)現(xiàn),創(chuàng)建多個(gè)線程陪每,每個(gè)線程執(zhí)行一個(gè)函數(shù)影晓,一個(gè)任務(wù)就這樣同時(shí)分由不同線程執(zhí)行了。

不要過多糾結(jié)多線程與多進(jìn)程檩禾、并發(fā)與并行這些概念 (這些概念都是相當(dāng)于多個(gè)人去合伙完成一件事)挂签,會用才是王道,理解大致意思即可,想要深入了解可閱讀本文“延伸拓展”章節(jié)盼产。

我們通常在何時(shí)使用并發(fā)? 程序使用并發(fā)的原因有兩種饵婆,為了關(guān)注點(diǎn)分離(程序中不同的功能,使用不同的線程去執(zhí)行)戏售,或者為了提高性能侨核。當(dāng)為了分離關(guān)注點(diǎn)而使用多線程時(shí),設(shè)計(jì)線程的數(shù)量的依據(jù)灌灾,不再是依賴于CPU中的可用內(nèi)核的數(shù)量搓译,而是依據(jù)概念上的設(shè)計(jì)(依據(jù)功能的劃分)。

知道何時(shí)不使用并發(fā)與知道何時(shí)使用它一樣重要锋喜。 不使用并發(fā)的唯一原因就是收益(性能的增幅)比不上成本(代碼開發(fā)的腦力成本些己、時(shí)間成本,代碼維護(hù)相關(guān)的額外成本)嘿般。運(yùn)行越多的線程轴总,操作系統(tǒng)需要為每個(gè)線程分配獨(dú)立的棧空間博个,需要越多的上下文切換,這會消耗很多操作系統(tǒng)資源功偿,如果在線程上的任務(wù)完成得很快盆佣,那么實(shí)際執(zhí)行任務(wù)的時(shí)間要比啟動線程的時(shí)間小很多往堡,所以在某些時(shí)候,增加一個(gè)額外的線程實(shí)際上會降低共耍,而非提高應(yīng)用程序的整體性能虑灰,此時(shí)收益就比不上成本。

2 C++多線程并發(fā)基礎(chǔ)知識

2.1 創(chuàng)建線程

首先要引入頭文件#include<thread>痹兜,C++11中管理線程的函數(shù)和類在該頭文件中聲明穆咐,其中包括std::thread類。

語句"std::thread th1(proc1);"創(chuàng)建了一個(gè)名為th1的線程字旭,并且線程th1開始執(zhí)行对湃。

實(shí)例化std::thread類對象時(shí),至少需要傳遞函數(shù)名作為參數(shù)遗淳。如果函數(shù)為有參函數(shù),如"void proc2(int a,int b)",那么實(shí)例化std::thread類對象時(shí)拍柒,則需要傳遞更多參數(shù),參數(shù)順序依次為函數(shù)名屈暗、該函數(shù)的第一個(gè)參數(shù)拆讯、該函數(shù)的第二個(gè)參數(shù),···养叛,如"std::thread th2(proc2,a,b);"种呐。這里的傳參,后續(xù)章節(jié)還會有詳解與提升弃甥。

只要創(chuàng)建了線程對象(前提是爽室,實(shí)例化std::thread對象時(shí)傳遞了“函數(shù)名/可調(diào)用對象”作為參數(shù)),線程就開始執(zhí)行潘飘。

總之肮之,使用C++線程庫啟動線程,可以歸結(jié)為構(gòu)造std::thread對象卜录。

那么至此一個(gè)簡單的多線程并發(fā)程序就編寫完了嗎戈擒?

不,還沒有艰毒。當(dāng)線程啟動后筐高,一定要在和線程相關(guān)聯(lián)的std::thread對象銷毀前,對線程運(yùn)用join()或者detach()方法丑瞧。

join()與detach()都是std::thread類的成員函數(shù)柑土,是兩種線程阻塞方法,兩者的區(qū)別是是否等待子線程執(zhí)行結(jié)束绊汹。

新手先把join()弄明白就行了稽屏,然后就可以去學(xué)習(xí)后面的章節(jié),等過段時(shí)間再回頭來學(xué)detach()西乖。

等待調(diào)用線程運(yùn)行結(jié)束后當(dāng)前線程再繼續(xù)運(yùn)行狐榔,例如坛增,主函數(shù)中有一條語句th1.join(),那么執(zhí)行到這里,主函數(shù)阻塞薄腻,直到線程th1運(yùn)行結(jié)束收捣,主函數(shù)再繼續(xù)運(yùn)行。

整個(gè)過程就相當(dāng)于:你在處理某件事情(你是主線程)庵楷,中途你讓老王幫你辦一個(gè)任務(wù)(與你同時(shí)執(zhí)行)(創(chuàng)建線程1罢艾,該線程取名老王),又叫老李幫你辦一件任務(wù)(創(chuàng)建線程2尽纽,該線程取名老李)咐蚯,現(xiàn)在你的一部分工作做完了,剩下的工作得用到他們的處理結(jié)果蜓斧,那就調(diào)用"老王.join()"與"老李.join()"仓蛆,至此你就需要等待(主線程阻塞),等他們把任務(wù)做完(子線程運(yùn)行結(jié)束)挎春,你就可以繼續(xù)你手頭的工作了(主線程不再阻塞)看疙。

一提到j(luò)oin,你腦海中就想起兩個(gè)字,"等待"直奋,而不是"加入"能庆,這樣就很容易理解join的功能。

#include<iostream>
#include<thread>
using namespace std;
void proc(int &a)
{
    cout << "我是子線程,傳入?yún)?shù)為" << a << endl;
    cout << "子線程中顯示子線程id為" << this_thread::get_id()<< endl;
}
int main()
{
    cout << "我是主線程" << endl;
    int a = 9;
    thread th2(proc,a);//第一個(gè)參數(shù)為函數(shù)名脚线,第二個(gè)參數(shù)為該函數(shù)的第一個(gè)參數(shù)搁胆,如果該函數(shù)接收多個(gè)參數(shù)就依次寫在后面。此時(shí)線程開始執(zhí)行邮绿。
    cout << "主線程中顯示子線程id為" << th2.get_id() << endl;
    th2.join()渠旁;//此時(shí)主線程被阻塞直至子線程執(zhí)行結(jié)束。
    return 0;
}

調(diào)用join()會清理線程相關(guān)的存儲部分船逮,這代表了join()只能調(diào)用一次顾腊。使用joinable()來判斷join()可否調(diào)用。同樣挖胃,detach()也只能調(diào)用一次杂靶,一旦detach()后就無法join()了,有趣的是酱鸭,detach()可否調(diào)用也是使用joinable()來判斷吗垮。

如果使用detach(),就必須保證線程結(jié)束之前可訪問數(shù)據(jù)的有效性凹髓,使用指針和引用需要格外謹(jǐn)慎烁登,這點(diǎn)我們放到以后再聊。

2.2 互斥量使用

什么是互斥量蔚舀?

這樣比喻:單位上有一臺打印機(jī)(共享數(shù)據(jù)a)饵沧,你要用打印機(jī)(線程1要操作數(shù)據(jù)a)蚀之,同事老王也要用打印機(jī)(線程2也要操作數(shù)據(jù)a),但是打印機(jī)同一時(shí)間只能給一個(gè)人用捷泞,此時(shí),規(guī)定不管是誰寿谴,在用打印機(jī)之前都要向領(lǐng)導(dǎo)申請?jiān)S可證(lock)锁右,用完后再向領(lǐng)導(dǎo)歸還許可證(unlock),許可證總共只有一個(gè),沒有許可證的人就等著在用打印機(jī)的同事用完后才能申請?jiān)S可證(阻塞讶泰,線程1lock互斥量后其他線程就無法lock,只能等線程1unlock后咏瑟,其他線程才能lock)。那么痪署,打印機(jī)就是共享數(shù)據(jù)码泞,訪問打印機(jī)的這段代碼就是臨界區(qū),這個(gè)必須互斥使用的許可證就是互斥量狼犯。

互斥量是為了解決數(shù)據(jù)共享過程中可能存在的訪問沖突的問題余寥。這里的互斥量保證了使用打印機(jī)這一過程不被打斷。

互斥量怎么使用悯森?

首先需要#include<mutex>宋舷;(std::mutex和std::lock_guard都在<mutex>頭文件中聲明。)

然后需要實(shí)例化std::mutex對象瓢姻;

最后需要在進(jìn)入臨界區(qū)之前對互斥量加鎖祝蝠,退出臨界區(qū)時(shí)對互斥量解鎖;

至此幻碱,互斥量走完了它的一生绎狭。

lock()與unlock():

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//實(shí)例化m對象,不要理解為定義變量
void proc1(int a)
{
    m.lock();
    cout << "proc1函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 2 << endl;
    m.unlock();
}

void proc2(int a)
{
    m.lock();
    cout << "proc2函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 1 << endl;
    m.unlock();
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

需要在進(jìn)入臨界區(qū)之前對互斥量lock褥傍,退出臨界區(qū)時(shí)對互斥量unlock儡嘶;當(dāng)一個(gè)線程使用特定互斥量鎖住共享數(shù)據(jù)時(shí),其他的線程想要訪問鎖住的數(shù)據(jù)摔桦,都必須等到之前那個(gè)線程對數(shù)據(jù)進(jìn)行解鎖后社付,才能進(jìn)行訪問。

程序?qū)嵗痬utex對象m,本線程調(diào)用成員函數(shù)m.lock()會發(fā)生下面 2 種情況: (1)如果該互斥量當(dāng)前未上鎖邻耕,則本線程將該互斥量鎖住鸥咖,直到調(diào)用unlock()之前,本線程一直擁有該鎖兄世。 (2)如果該互斥量當(dāng)前被其他線程鎖住啼辣,則本線程被阻塞,直至該互斥量被其他線程解鎖,此時(shí)本線程將該互斥量鎖住御滩,直到調(diào)用unlock()之前鸥拧,本線程一直擁有該鎖党远。

不推薦實(shí)直接去調(diào)用成員函數(shù)lock(),因?yàn)槿绻泆nlock()富弦,將導(dǎo)致鎖無法釋放沟娱,使用lock_guard或者unique_lock則能避免忘記解鎖帶來的問題。

lock_guard:
std::lock_guard()是什么呢腕柜?它就像一個(gè)保姆济似,職責(zé)就是幫你管理互斥量,就好像小孩要玩玩具時(shí)候盏缤,保姆就幫忙把玩具找出來砰蠢,孩子不玩了,保姆就把玩具收納好唉铜。

其原理是:聲明一個(gè)局部的std::lock_guard對象台舱,在其構(gòu)造函數(shù)中進(jìn)行加鎖,在其析構(gòu)函數(shù)中進(jìn)行解鎖潭流。最終的結(jié)果就是:創(chuàng)建即加鎖竞惋,作用域結(jié)束自動解鎖。從而使用std::lock_guard()就可以替代lock()與unlock()幻枉。

通過設(shè)定作用域碰声,使得std::lock_guard在合適的地方被析構(gòu)(在互斥量鎖定到互斥量解鎖之間的代碼叫做臨界區(qū)(需要互斥訪問共享資源的那段代碼稱為臨界區(qū)),臨界區(qū)范圍應(yīng)該盡可能的小熬甫,即lock互斥量后應(yīng)該盡早unlock)胰挑,通過使用{}來調(diào)整作用域范圍,可使得互斥量m在合適的地方被解鎖:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//實(shí)例化m對象椿肩,不要理解為定義變量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此語句替換了m.lock()瞻颂;lock_guard傳入一個(gè)參數(shù)時(shí),該參數(shù)為互斥量郑象,此時(shí)調(diào)用了lock_guard的構(gòu)造函數(shù)贡这,申請鎖定m
    cout << "proc1函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 2 << endl;
}//此時(shí)不需要寫m.unlock(),g1出了作用域被釋放,自動調(diào)用析構(gòu)函數(shù)厂榛,于是m被解鎖

void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函數(shù)正在改寫a" << endl;
        cout << "原始a為" << a << endl;
        cout << "現(xiàn)在a為" << a + 1 << endl;
    }//通過使用{}來調(diào)整作用域范圍盖矫,可使得m在合適的地方被解鎖
    cout << "作用域外的內(nèi)容3" << endl;
    cout << "作用域外的內(nèi)容4" << endl;
    cout << "作用域外的內(nèi)容5" << endl;
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

std::lock_gurad也可以傳入兩個(gè)參數(shù),第一個(gè)參數(shù)為adopt_lock標(biāo)識時(shí)击奶,表示構(gòu)造函數(shù)中不再進(jìn)行互斥量鎖定辈双,因此此時(shí)需要提前手動鎖定。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//實(shí)例化m對象柜砾,不要理解為定義變量
void proc1(int a)
{
    m.lock();//手動鎖定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 2 << endl;
}//自動解鎖

void proc2(int a)
{
    lock_guard<mutex> g2(m);//自動鎖定
    cout << "proc2函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 1 << endl;
}//自動解鎖
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

unique_lock:
std::unique_lock類似于lock_guard,只是std::unique_lock用法更加豐富湃望,同時(shí)支持std::lock_guard()的原有功能。 使用std::lock_guard后不能手動lock()與手動unlock();使用std::unique_lock后可以手動lock()與手動unlock(); std::unique_lock的第二個(gè)參數(shù),除了可以是adopt_lock,還可以是try_to_lock與defer_lock;
嘗試用mutx的lock()去鎖定這個(gè)mutex证芭,但如果沒有鎖定成功瞳浦,會立即返回,不會阻塞在那里废士,并繼續(xù)往下執(zhí)行叫潦;

defer_lock: 始化了一個(gè)沒有加鎖的mutex;


image.png
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);//始化了一個(gè)沒有加鎖的mutex
    cout << "xxxxxxxx" << endl;
    g1.lock();//手動加鎖,注意官硝,不是m.lock();注意诅挑,不是m.lock(),m已經(jīng)被g1接管了;
    cout << "proc1函數(shù)正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現(xiàn)在a為" << a + 2 << endl;
    g1.unlock();//臨時(shí)解鎖
    cout << "xxxxx"  << endl;
    g1.lock();
    cout << "xxxxxx" << endl;
}//自動解鎖

void proc2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//嘗試加鎖一次,但如果沒有鎖定成功泛源,會立即返回,不會阻塞在那里忿危,且不會再次嘗試鎖操作达箍。
    if(g2.owns_lock){//鎖成功
        cout << "proc2函數(shù)正在改寫a" << endl;
        cout << "原始a為" << a << endl;
        cout << "現(xiàn)在a為" << a + 1 << endl;
    }else{//鎖失敗則執(zhí)行這段語句
        cout <<""<<endl;
    }
}//自動解鎖

int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

使用try_to_lock要小心,因?yàn)閠ry_to_lock嘗試鎖失敗后不會阻塞線程铺厨,而是繼續(xù)往下執(zhí)行程序缎玫,因此,需要使用if-else語句來判斷是否鎖成功,只有鎖成功后才能去執(zhí)行互斥代碼段解滓。而且需要注意的是赃磨,因?yàn)閠ry_to_lock嘗試鎖失敗后代碼繼續(xù)往下執(zhí)行了,因此該語句不會再次去嘗試鎖洼裤。

std::unique_lock所有權(quán)的轉(zhuǎn)移

注意邻辉,這里的轉(zhuǎn)移指的是std::unique_lock對象間的轉(zhuǎn)移;std::mutex對象的所有權(quán)不需要手動轉(zhuǎn)移給std::unique_lock , std::unique_lock對象實(shí)例化后會直接接管std::mutex腮鞍。

mutex m;
{  
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有權(quán)轉(zhuǎn)移值骇,此時(shí)由g3來管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}

condition_variable:

需要#include<condition_variable>,該頭文件中包含了條件變量相關(guān)的類移国,其中包括std::condition_variable類

如何使用吱瘩?std::condition_variable類搭配std::mutex類來使用,std::condition_variable對象(std::condition_variable cond;)的作用不是用來管理互斥量的迹缀,它的作用是用來同步線程使碾,它的用法相當(dāng)于編程中常見的flag標(biāo)志(A、B兩個(gè)人約定flag=true為行動號角祝懂,默認(rèn)flag為false,A不斷的檢查flag的值,只要B將flag修改為true票摇,A就開始行動)。

類比到std::condition_variable嫂易,A兄朋、B兩個(gè)人約定notify_one為行動號角,A就等著(調(diào)用wait(),阻塞),只要B一調(diào)用notify_one,A就開始行動(不再阻塞)颅和。

std::condition_variable的具體使用代碼實(shí)例可以參見文章中“生產(chǎn)者與消費(fèi)者問題”章節(jié)傅事。

wait(locker) :

wait函數(shù)需要傳入一個(gè)std::mutex(一般會傳入std::unique_lock對象),即上述的locker。wait函數(shù)會自動調(diào)用 locker.unlock() 釋放鎖(因?yàn)樾枰尫沛i峡扩,所以要傳入mutex)并阻塞當(dāng)前線程蹭越,本線程釋放鎖使得其他的線程得以繼續(xù)競爭鎖。一旦當(dāng)前線程獲得notify(通常是另外某個(gè)線程調(diào)用 notify_* 喚醒了當(dāng)前線程)教届,wait() 函數(shù)此時(shí)再自動調(diào)用 locker.lock()上鎖响鹃。

cond.notify_one(): 隨機(jī)喚醒一個(gè)等待的線程

cond.notify_all(): 喚醒所有等待的線程

2.3 異步線程

需要#include<future>

async與future:

std::async是一個(gè)函數(shù)模板,用來啟動一個(gè)異步任務(wù)案训,它返回一個(gè)std::future類模板對象买置,future對象起到了占位的作用(記住這點(diǎn)就可以了),占位是什么意思强霎?就是說該變量現(xiàn)在無值忿项,但將來會有值(好比你擠公交瞧見空了個(gè)座位,剛準(zhǔn)備坐下去就被旁邊的小伙給攔住了:“這個(gè)座位有人了”城舞,你反駁道:”這不是空著嗎轩触?“,小伙:”等會人就來了“),剛實(shí)例化的future是沒有儲存值的家夺,但在調(diào)用std::future對象的get()成員函數(shù)時(shí)脱柱,主線程會被阻塞直到異步線程執(zhí)行結(jié)束,并把返回結(jié)果傳遞給std::future拉馋,即通過FutureObject.get()獲取函數(shù)返回值榨为。

相當(dāng)于你去辦政府辦業(yè)務(wù)(主線程),把資料交給了前臺煌茴,前臺安排了人員去給你辦理(std::async創(chuàng)建子線程)柠逞,前臺給了你一個(gè)單據(jù)(std::future對象),說你的業(yè)務(wù)正在給你辦(子線程正在運(yùn)行)景馁,等段時(shí)間你再過來憑這個(gè)單據(jù)取結(jié)果板壮。過了段時(shí)間,你去前臺取結(jié)果(調(diào)用get())合住,但是結(jié)果還沒出來(子線程還沒return)绰精,你就在前臺等著(阻塞),直到你拿到結(jié)果(子線程return)透葛,你才離開(不再阻塞)笨使。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
 double c = a + b;
 Sleep(3000);//假設(shè)t1函數(shù)是個(gè)復(fù)雜的計(jì)算過程,需要消耗3秒
 return c;
}

int main() 
{
 double a = 2.3;
 double b = 6.7;
 future<double> fu = async(t1, a, b);//創(chuàng)建異步線程線程僚害,并將線程的執(zhí)行結(jié)果用fu占位硫椰;
 cout << "正在進(jìn)行計(jì)算" << endl;
 cout << "計(jì)算結(jié)果馬上就準(zhǔn)備好,請您耐心等待" << endl;
 cout << "計(jì)算結(jié)果:" << fu.get() << endl;//阻塞主線程,直至異步線程return
        //cout << "計(jì)算結(jié)果:" << fu.get() << endl;//取消該語句注釋后運(yùn)行會報(bào)錯(cuò)靶草,因?yàn)閒uture對象的get()方法只能調(diào)用一次蹄胰。
 return 0;
}

shared_future

std::future與std::shard_future的用途都是為了占位,但是兩者有些許差別奕翔。std::future的get()成員函數(shù)是轉(zhuǎn)移數(shù)據(jù)所有權(quán);std::shared_future的get()成員函數(shù)是復(fù)制數(shù)據(jù)裕寨。 因此: future對象的get()只能調(diào)用一次;無法實(shí)現(xiàn)多個(gè)線程等待同一個(gè)異步線程派继,一旦其中一個(gè)線程獲取了異步線程的返回值宾袜,其他線程就無法再次獲取。 std::shared_future對象的get()可以調(diào)用多次驾窟;可以實(shí)現(xiàn)多個(gè)線程等待同一個(gè)異步線程庆猫,每個(gè)線程都可以獲取異步線程的返回值。


image.png

2.4 原子類型atomic<>

原子操作指“不可分割的操作”绅络,也就是說這種操作狀態(tài)要么是完成的阅悍,要么是沒完成的,不存在“操作完成了一半”這種狀況昨稼。互斥量的加鎖一般是針對一個(gè)代碼段拳锚,而原子操作針對的一般都是一個(gè)變量(操作變量時(shí)加鎖防止他人干擾)假栓。 std::atomic<>是一個(gè)模板類,使用該模板類實(shí)例化的對象霍掺,提供了一些保證原子性的成員函數(shù)來實(shí)現(xiàn)共享數(shù)據(jù)的常用操作匾荆。

可以這樣理解: 在以前,定義了一個(gè)共享的變量(int i=0)杆烁,多個(gè)線程會用到這個(gè)變量牙丽,那么每次操作這個(gè)變量時(shí),都需要lock加鎖兔魂,操作完畢unlock解鎖烤芦,以保證線程之間不會沖突;但是這樣每次加鎖解鎖析校、加鎖解鎖就顯得很麻煩构罗,那怎么辦呢? 現(xiàn)在智玻,實(shí)例化了一個(gè)類對象(std::atomic<int> I=0)來代替以前的那個(gè)變量(這里的對象I你就把它看作一個(gè)變量遂唧,看作對象反而難以理解了),每次操作這個(gè)對象時(shí)吊奢,就不用lock與unlock盖彭,這個(gè)對象自身就具有原子性(相當(dāng)于加鎖解鎖操作不用你寫代碼實(shí)現(xiàn),能自動加鎖解鎖了),以保證線程之間不會沖突召边。

提到std::atomic<>铺呵,你腦海里就想到一點(diǎn)就可以了:std::atomic<>用來定義一個(gè)自動加鎖解鎖的共享變量(“定義”“變量”用詞在這里是不準(zhǔn)確的,但是更加貼切它的實(shí)際功能)掌实,供多個(gè)線程訪問而不發(fā)生沖突陪蜻。

//原子類型的簡單使用
std::atomic<bool> b(true);
b=false;

std::atomic<>對象提供了常見的原子操作(通過調(diào)用成員函數(shù)實(shí)現(xiàn)對數(shù)據(jù)的原子操作): store是原子寫操作,load是原子讀操作贱鼻。exchange是于兩個(gè)數(shù)值進(jìn)行交換的原子操作宴卖。 即使使用了std::atomic<>,也要注意執(zhí)行的操作是否支持原子性邻悬,也就是說症昏,你不要覺得用的是具有原子性的變量(準(zhǔn)確說是對象)就可以為所欲為了,你對它進(jìn)行的運(yùn)算不支持原子性的話父丰,也不能實(shí)現(xiàn)其原子效果肝谭。一般針對++,–蛾扇,+=攘烛,-=,&=镀首,|=坟漱,^=是支持的,這些原子操作是通過在std::atomic<>對象內(nèi)部進(jìn)行運(yùn)算符重載實(shí)現(xiàn)的更哄。

3 代碼實(shí)例

3.1 生產(chǎn)者消費(fèi)者問題
生產(chǎn)者-消費(fèi)者模型是經(jīng)典的多線程并發(fā)協(xié)作模型芋齿。生產(chǎn)者用于生產(chǎn)數(shù)據(jù),生產(chǎn)一個(gè)就往共享數(shù)據(jù)區(qū)存一個(gè)成翩,如果共享數(shù)據(jù)區(qū)已滿的話觅捆,生產(chǎn)者就暫停生產(chǎn);消費(fèi)者用于消費(fèi)數(shù)據(jù)麻敌,一個(gè)一個(gè)的從共享數(shù)據(jù)區(qū)取栅炒,如果共享數(shù)據(jù)區(qū)為空的話,消費(fèi)者就暫停取數(shù)據(jù)术羔,且生產(chǎn)者與消費(fèi)者不能直接交互职辅。

/*
生產(chǎn)者消費(fèi)者問題
*/
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include<Windows.h>
using namespace std;

deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//緩沖區(qū)的產(chǎn)品個(gè)數(shù)

void producer() { 
 int data1;
 while (1) {//通過外層循環(huán),能保證生產(chǎn)不停止
  if(c < 3) {//限流
   {
    data1 = rand();
    unique_lock<mutex> locker(mu);//鎖
    q.push_front(data1);
    cout << "存了" << data1 << endl;
    cond.notify_one();  // 通知取
    ++c;
   }
   Sleep(500);
  }
 }
}

void consumer() {
 int data2;//data用來覆蓋存放取的數(shù)據(jù)
 while (1) {
  {
   unique_lock<mutex> locker(mu);
   while(q.empty())
    cond.wait(locker); //wait()阻塞前先會解鎖,解鎖后生產(chǎn)者才能獲得鎖來放產(chǎn)品到緩沖區(qū)聂示;生產(chǎn)者notify后域携,將不再阻塞,且自動又獲得了鎖鱼喉。
   data2 = q.back();//取的第一步
   q.pop_back();//取的第二步
   cout << "取了" << data2<<endl;
   --c;
  }
  Sleep(1500);
 }
}
int main() {
 thread t1(producer);
 thread t2(consumer);
 t1.join();
 t2.join();
 return 0;
}

4 C++多線程并發(fā)高級知識

4.1 線程池

4.1.1 線程池基礎(chǔ)知識

不采用線程池時(shí):

創(chuàng)建線程 -> 由該線程執(zhí)行任務(wù) -> 任務(wù)執(zhí)行完畢后銷毀線程秀鞭。即使需要使用到大量線程趋观,每個(gè)線程都要按照這個(gè)流程來創(chuàng)建、執(zhí)行與銷毀锋边。

雖然創(chuàng)建與銷毀線程消耗的時(shí)間 遠(yuǎn)小于 線程執(zhí)行的時(shí)間皱坛,但是對于需要頻繁創(chuàng)建大量線程的任務(wù),創(chuàng)建與銷毀線程 所占用的時(shí)間與CPU資源也會有很大占比豆巨。

為了減少創(chuàng)建與銷毀線程所帶來的時(shí)間消耗與資源消耗剩辟,因此采用線程池的策略:

程序啟動后,預(yù)先創(chuàng)建一定數(shù)量的線程放入空閑隊(duì)列中往扔,這些線程都是處于阻塞狀態(tài)贩猎,基本不消耗CPU,只占用較小的內(nèi)存空間萍膛。

接收到任務(wù)后吭服,任務(wù)被掛在任務(wù)隊(duì)列,線程池選擇一個(gè)空閑線程來執(zhí)行此任務(wù)蝗罗。

任務(wù)執(zhí)行完畢后艇棕,不銷毀線程,線程繼續(xù)保持在池中等待下一次的任務(wù)串塑。

線程池所解決的問題:

(1) 需要頻繁創(chuàng)建與銷毀大量線程的情況下沼琉,由于線程預(yù)先就創(chuàng)建好了,接到任務(wù)就能馬上從線程池中調(diào)用線程來處理任務(wù)桩匪,減少了創(chuàng)建與銷毀線程帶來的時(shí)間開銷和CPU資源占用打瘪。

(2) 需要并發(fā)的任務(wù)很多時(shí)候,無法為每個(gè)任務(wù)指定一個(gè)線程(線程不夠分)吸祟,使用線程池可以將提交的任務(wù)掛在任務(wù)隊(duì)列上,等到池中有空閑線程時(shí)就可以為該任務(wù)指定線程桃移。

4.1.2 線程池的實(shí)現(xiàn)

可以通過閱讀 《C++ Concurrency in Action, Second Edition》 9.1章節(jié)來學(xué)習(xí)屋匕。線程池確實(shí)是難點(diǎn)部分,所以先拖著不更借杰,等把別的章節(jié)完善了过吻,再來更新這部分。蔗衡, 本文的線程池實(shí)現(xiàn)的內(nèi)容將會在《C++11 STL基礎(chǔ)入門教程》完善后再來更新纤虽。

學(xué)習(xí)交流群整理了一些最新LinuxC/C++服務(wù)器開發(fā)/架構(gòu)師面試題、學(xué)習(xí)資料绞惦、教學(xué)視頻和學(xué)習(xí)路線腦圖(資料包括C/C++逼纸,Linux,golang技術(shù)济蝉,Nginx杰刽,ZeroMQ菠发,MySQL,Redis贺嫂,fastdfs滓鸠,MongoDB,ZK第喳,流媒體糜俗,CDN,P2P曲饱,K8S悠抹,Docker,TCP/IP渔工,協(xié)程锌钮,DPDK,ffmpeg等)引矩,免費(fèi)分享有需要的可以自行添加

image.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末梁丘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子旺韭,更是在濱河造成了極大的恐慌氛谜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件区端,死亡現(xiàn)場離奇詭異值漫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)织盼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門杨何,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沥邻,你說我怎么就攤上這事危虱。” “怎么了唐全?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵埃跷,是天一觀的道長。 經(jīng)常有香客問我邮利,道長弥雹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任延届,我火速辦了婚禮剪勿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘方庭。我一直安慰自己窗宦,他們只是感情好赦颇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赴涵,像睡著了一般媒怯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上髓窜,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天扇苞,我揣著相機(jī)與錄音,去河邊找鬼寄纵。 笑死鳖敷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的程拭。 我是一名探鬼主播定踱,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恃鞋!你這毒婦竟也來了崖媚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤恤浪,失蹤者是張志新(化名)和其女友劉穎畅哑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體水由,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡荠呐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砂客。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泥张。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鞠值,靈堂內(nèi)的尸體忽然破棺而出媚创,到底是詐尸還是另有隱情,我是刑警寧澤齿诉,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布筝野,位于F島的核電站晌姚,受9級特大地震影響粤剧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挥唠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一抵恋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宝磨,春花似錦弧关、人聲如沸盅安。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽别瞭。三九已至,卻和暖如春株憾,著一層夾襖步出監(jiān)牢的瞬間蝙寨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工嗤瞎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留墙歪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓贝奇,卻偏偏與公主長得像虹菲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子掉瞳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容