什么是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;
#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è)線程都可以獲取異步線程的返回值。
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)分享有需要的可以自行添加