C++多线程详细讲解
本文是純轉(zhuǎn)載,覺得大佬寫的非常好!如有侵權(quán)可以刪除
鏈接: link.
C++多線程基礎(chǔ)教程
目錄
1 什么是C++多線程?
2 C++多線程基礎(chǔ)知識(shí)
2.1 創(chuàng)建線程
2.2 互斥量使用
lock()與unlock():
lock_guard():
unique_lock:
condition_variable:
2.3 異步線程
async與future:
shared_future
2.4 原子類型automic
實(shí)例
生產(chǎn)者消費(fèi)者問題
4 C++多線程高級(jí)知識(shí)
4.1 線程池
線程池基礎(chǔ)知識(shí)
線程池的實(shí)現(xiàn)
5 延伸拓展
最后一次更新日期:2020.08.23
1 什么是C++多線程?
線程:線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,它被包含在進(jìn)程之中,進(jìn)程包含一個(gè)或者多個(gè)線程。進(jìn)程可以理解為完成一件事的完整解決方案,而線程可以理解為這個(gè)解決方案中的的一個(gè)步驟,可能這個(gè)解決方案就這只有一個(gè)步驟,也可能這個(gè)解決方案有多個(gè)步驟。
多線程:多線程是實(shí)現(xiàn)并發(fā)(并行)的手段,并發(fā)(并行)即多個(gè)線程同時(shí)執(zhí)行,一般而言,多線程就是把執(zhí)行一件事情的完整步驟拆分為多個(gè)子步驟,然后使得這多個(gè)步驟同時(shí)執(zhí)行。
C++多線程:(簡單情況下)C++多線程使用多個(gè)函數(shù)實(shí)現(xiàn)各自功能,然后將不同函數(shù)生成不同線程,并同時(shí)執(zhí)行這些線程(不同線程可能存在一定程度的執(zhí)行先后順序,但總體上可以看做同時(shí)執(zhí)行)。
上述概念很容易因表述不準(zhǔn)確而造成誤解,這里沒有深究線程與進(jìn)程,并發(fā)與并行的概念,以上僅為一種便于理解的表述,如果有任何問題還請(qǐng)指正,若有更好的表述,也歡迎留言分享。
2 C++多線程基礎(chǔ)知識(shí)
2.1 創(chuàng)建線程
首先要引入頭文件#include(C++11的標(biāo)準(zhǔn)庫中提供了多線程庫),該頭文件中定義了thread類,創(chuàng)建一個(gè)線程即實(shí)例化一個(gè)該類的對(duì)象,實(shí)例化對(duì)象時(shí)候調(diào)用的構(gòu)造函數(shù)需要傳遞一個(gè)參數(shù),該參數(shù)就是函數(shù)名,thread th1(proc1);如果傳遞進(jìn)去的函數(shù)本身需要傳遞參數(shù),實(shí)例化對(duì)象時(shí)將這些參數(shù)按序?qū)懙胶瘮?shù)名后面,thread th1(proc1,a,b);只要?jiǎng)?chuàng)建了線程對(duì)象(傳遞“函數(shù)名/可調(diào)用對(duì)象”作為參數(shù)的情況下),線程就開始執(zhí)行(std::thread 有一個(gè)無參構(gòu)造函數(shù)重載的版本,不會(huì)創(chuàng)建底層的線程)。
有兩種線程阻塞方法join()與detach(),阻塞線程的目的是調(diào)節(jié)各線程的先后執(zhí)行順序,這里重點(diǎn)講join()方法,不推薦使用detach(),detach()使用不當(dāng)會(huì)發(fā)生引用對(duì)象失效的錯(cuò)誤。當(dāng)線程啟動(dòng)后,一定要在和線程相關(guān)聯(lián)的thread對(duì)象銷毀前,對(duì)線程運(yùn)用join()或者detach()。
join(), 當(dāng)前線程暫停, 等待指定的線程執(zhí)行結(jié)束后, 當(dāng)前線程再繼續(xù)。th1.join(),即該語句所在的線程(該語句寫在main()函數(shù)里面,即主線程內(nèi)部)暫停,等待指定線程(指定線程為th1)執(zhí)行結(jié)束后,主線程再繼續(xù)執(zhí)行。
整個(gè)過程就相當(dāng)于你在做某件事情,中途你讓老王幫你辦一個(gè)任務(wù)(你辦的時(shí)候他同時(shí)辦)(創(chuàng)建線程1),又叫老李幫你辦一件任務(wù)(創(chuàng)建線程2),現(xiàn)在你的這部分工作做完了,需要用到他們的結(jié)果,只需要等待老王和老李處理完(join(),阻塞主線程),等他們把任務(wù)做完(子線程運(yùn)行結(jié)束),你又可以開始你手頭的工作了(主線程不再阻塞)。
2.2 互斥量使用
什么是互斥量?
這樣比喻,單位上有一臺(tái)打印機(jī)(共享數(shù)據(jù)a),你要用打印機(jī)(線程1要操作數(shù)據(jù)a),同事老王也要用打印機(jī)(線程2也要操作數(shù)據(jù)a),但是打印機(jī)同一時(shí)間只能給一個(gè)人用,此時(shí),規(guī)定不管是誰,在用打印機(jī)之前都要向領(lǐng)導(dǎo)申請(qǐng)?jiān)S可證(lock),用完后再向領(lǐng)導(dǎo)歸還許可證(unlock),許可證總共只有一個(gè),沒有許可證的人就等著在用打印機(jī)的同事用完后才能申請(qǐng)?jiān)S可證(阻塞,線程1lock互斥量后其他線程就無法lock,只能等線程1unlock后,其他線程才能lock),那么,這個(gè)許可證就是互斥量。互斥量保證了使用打印機(jī)這一過程不被打斷。
程序?qū)嵗痬utex對(duì)象m,線程調(diào)用成員函數(shù)m.lock()會(huì)發(fā)生下面 3 種情況:
(1)如果該互斥量當(dāng)前未上鎖,則調(diào)用線程將該互斥量鎖住,直到調(diào)用unlock()之前,該線程一直擁有該鎖。
(2)如果該互斥量當(dāng)前被鎖住,則調(diào)用線程被阻塞,直至該互斥量被解鎖。
互斥量怎么使用?
首先需要#include
lock()與unlock():
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//實(shí)例化m對(duì)象,不要理解為定義變量 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; }不推薦實(shí)直接去調(diào)用成員函數(shù)lock(),因?yàn)槿绻泆nlock(),將導(dǎo)致鎖無法釋放,使用lock_guard或者unique_lock能避免忘記解鎖這種問題。
lock_guard():
其原理是:聲明一個(gè)局部的lock_guard對(duì)象,在其構(gòu)造函數(shù)中進(jìn)行加鎖,在其析構(gòu)函數(shù)中進(jìn)行解鎖。最終的結(jié)果就是:創(chuàng)建即加鎖,作用域結(jié)束自動(dòng)解鎖。從而使用lock_guard()就可以替代lock()與unlock()。
通過設(shè)定作用域,使得lock_guard在合適的地方被析構(gòu)(在互斥量鎖定到互斥量解鎖之間的代碼叫做臨界區(qū)(需要互斥訪問共享資源的那段代碼稱為臨界區(qū)),臨界區(qū)范圍應(yīng)該盡可能的小,即lock互斥量后應(yīng)該盡早unlock),通過使用{}來調(diào)整作用域范圍,可使得互斥量m在合適的地方被解鎖:
lock_gurad也可以傳入兩個(gè)參數(shù),第一個(gè)參數(shù)為adopt_lock標(biāo)識(shí)時(shí),表示不再構(gòu)造函數(shù)中不再進(jìn)行互斥量鎖定,因此此時(shí)需要提前手動(dòng)鎖定。
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//實(shí)例化m對(duì)象,不要理解為定義變量 void proc1(int a) {m.lock();//手動(dòng)鎖定lock_guard<mutex> g1(m,adopt_lock);cout << "proc1函數(shù)正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現(xiàn)在a為" << a + 2 << endl; }//自動(dòng)解鎖void proc2(int a) {lock_guard<mutex> g2(m);//自動(dòng)鎖定cout << "proc2函數(shù)正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現(xiàn)在a為" << a + 1 << endl; }//自動(dòng)解鎖 int main() {int a = 0;thread proc1(proc1, a);thread proc2(proc2, a);proc1.join();proc2.join();return 0; }unique_lock:
unique_lock類似于lock_guard,只是unique_lock用法更加豐富,同時(shí)支持lock_guard()的原有功能。
使用lock_guard后不能手動(dòng)lock()與手動(dòng)unlock();使用unique_lock后可以手動(dòng)lock()與手動(dòng)unlock();
unique_lock的第二個(gè)參數(shù),除了可以是adopt_lock,還可以是try_to_lock與defer_lock;
try_to_lock: 嘗試去鎖定,得保證鎖處于unlock的狀態(tài),然后嘗試現(xiàn)在能不能獲得鎖;嘗試用mutx的lock()去鎖定這個(gè)mutex,但如果沒有鎖定成功,會(huì)立即返回,不會(huì)阻塞在那里
defer_lock: 始化了一個(gè)沒有加鎖的mutex;
lock_guard unique_lock
手動(dòng)lock與手動(dòng)unlock 不支持 支持
參數(shù) 支持adopt_lock 支持adopt_lock/try_to_lock/defer_lock
condition_variable:
需要#include<condition_variable>;
wait(locker):在線程被阻塞時(shí),該函數(shù)會(huì)自動(dòng)調(diào)用 locker.unlock() 釋放鎖,使得其他被阻塞在鎖競爭上的線程得以繼續(xù)執(zhí)行。另外,一旦當(dāng)前線程獲得通知(通常是另外某個(gè)線程調(diào)用 notify_* 喚醒了當(dāng)前線程),wait() 函數(shù)此時(shí)再自動(dòng)調(diào)用 locker.lock()。
notify_all():隨機(jī)喚醒一個(gè)等待的線程
notify_once():喚醒所有等待的線程
2.3 異步線程
需要#include
async與future:
async是一個(gè)函數(shù)模板,用來啟動(dòng)一個(gè)異步任務(wù),它返回一個(gè)future類模板對(duì)象,future對(duì)象起到了占位的作用,剛實(shí)例化的future是沒有儲(chǔ)存值的,但在調(diào)用future對(duì)象的get()成員函數(shù)時(shí),主線程會(huì)被阻塞直到異步線程執(zhí)行結(jié)束,并把返回結(jié)果傳遞給future,即通過FutureObject.get()獲取函數(shù)返回值。
相當(dāng)于你去辦政府辦業(yè)務(wù)(主線程),把資料交給了前臺(tái),前臺(tái)安排了人員去給你辦理(async創(chuàng)建子線程),前臺(tái)給了你一個(gè)單據(jù)(future對(duì)象),說你的業(yè)務(wù)正在給你辦(子線程正在運(yùn)行),等段時(shí)間你再過來憑這個(gè)單據(jù)取結(jié)果。過了段時(shí)間,你去前臺(tái)取結(jié)果,但是結(jié)果還沒出來(子線程還沒return),你就在前臺(tái)等著(阻塞),直到你拿到結(jié)果(get())你才離開(不再阻塞)。
#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)備好,請(qǐng)您耐心等待" << endl;cout << "計(jì)算結(jié)果:" << fu.get() << endl;//阻塞主線程,直至異步線程return//cout << "計(jì)算結(jié)果:" << fu.get() << endl;//取消該語句注釋后運(yùn)行會(huì)報(bào)錯(cuò),因?yàn)閒uture對(duì)象的get()方法只能調(diào)用一次。return 0; }shared_future
future與shard_future的用途都是為了占位,但是兩者有些許差別。
future的get()成員函數(shù)是轉(zhuǎn)移數(shù)據(jù)所有權(quán);shared_future的get()成員函數(shù)是復(fù)制數(shù)據(jù)。
因此:
future對(duì)象的get()只能調(diào)用一次;無法實(shí)現(xiàn)多個(gè)線程等待同一個(gè)異步線程,一旦其中一個(gè)線程獲取了異步線程的返回值,其他線程就無法再次獲取。
shared_future對(duì)象的get()可以調(diào)用多次;可以實(shí)現(xiàn)多個(gè)線程等待同一個(gè)異步線程,每個(gè)線程都可以獲取異步線程的返回值。
future shared_future
語義 轉(zhuǎn)移 賦值
可否多次調(diào)用 否 可
2.4 原子類型automic
原子操作指“不可分割的操作”;也就是說這種操作狀態(tài)要么是完成的,要么是沒完成的。互斥量的加鎖一般是針對(duì)一個(gè)代碼段,而原子操作針對(duì)的一般都是一個(gè)變量。
automic是一個(gè)模板類,使用該模板類實(shí)例化的對(duì)象,提供了一些保證原子性的成員函數(shù)來實(shí)現(xiàn)共享數(shù)據(jù)的常用操作。
可以這樣理解:
在以前,定義了一個(gè)共享的變量(int i=0),多個(gè)線程會(huì)操作這個(gè)變量,那么每次操作這個(gè)變量時(shí),都是用lock加鎖,操作完畢使用unlock解鎖,以保證線程之間不會(huì)沖突;
現(xiàn)在,實(shí)例化了一個(gè)類對(duì)象(automic I=0)來代替以前的那個(gè)變量,每次操作這個(gè)對(duì)象時(shí),就不用lock與unlock,這個(gè)對(duì)象自身就具有原子性,以保證線程之間不會(huì)沖突。
automic對(duì)象提供了常見的原子操作(通過調(diào)用成員函數(shù)實(shí)現(xiàn)對(duì)數(shù)據(jù)的原子操作):
store是原子寫操作,load是原子讀操作。exchange是于兩個(gè)數(shù)值進(jìn)行交換的原子操作。
即使使用了automic,也要注意執(zhí)行的操作是否支持原子性。一般atomic原子操作,針對(duì)++,–,+=,-=,&=,|=,^=是支持的。
實(shí)例
前一章內(nèi)容為了簡單的說明一些函數(shù)的用法,所列舉的例子有些牽強(qiáng),因此在本章列舉了一些多線程常見的實(shí)例
生產(chǎn)者消費(fèi)者問題
/*
4 C++多線程高級(jí)知識(shí)
4.1 線程池
線程池基礎(chǔ)知識(shí)
不采用線程池時(shí):
創(chuàng)建線程 -> 由該線程執(zhí)行任務(wù) -> 任務(wù)執(zhí)行完畢后銷毀線程。即使需要使用到大量線程,每個(gè)線程都要按照這個(gè)流程來創(chuàng)建、執(zhí)行與銷毀。
雖然創(chuàng)建與銷毀線程消耗的時(shí)間 遠(yuǎn)小于 線程執(zhí)行的時(shí)間,但是對(duì)于需要頻繁創(chuàng)建大量線程的任務(wù),創(chuàng)建與銷毀線程 所占用的時(shí)間與CPU資源也會(huì)有很大占比。
為了減少創(chuàng)建與銷毀線程所帶來的時(shí)間消耗與資源消耗,因此采用線程池的策略:
程序啟動(dòng)后,預(yù)先創(chuàng)建一定數(shù)量的線程放入空閑隊(duì)列中,這些線程都是處于阻塞狀態(tài),基本不消耗CPU,只占用較小的內(nèi)存空間。
接收到任務(wù)后,線程池選擇一個(gè)空閑線程來執(zhí)行此任務(wù)。
任務(wù)執(zhí)行完畢后,不銷毀線程,線程繼續(xù)保持在池中等待下一次的任務(wù)。
線程池所解決的問題:
(1) 需要頻繁創(chuàng)建與銷毀大量線程的情況下,減少了創(chuàng)建與銷毀線程帶來的時(shí)間開銷和CPU資源占用。(省時(shí)省力)
(2) 實(shí)時(shí)性要求較高的情況下,由于大量線程預(yù)先就創(chuàng)建好了,接到任務(wù)就能馬上從線程池中調(diào)用線程來處理任務(wù),略過了創(chuàng)建線程這一步驟,提高了實(shí)時(shí)性。(實(shí)時(shí))
線程池的實(shí)現(xiàn)
待更新。
延伸拓展
創(chuàng)建類,除了傳遞函數(shù)外,還可以使用:Lambda表達(dá)式、可調(diào)用類的實(shí)例。
線程與進(jìn)程:
并發(fā)與并行:
并發(fā)與并行并不是非此即彼的概念
并發(fā):同一時(shí)間發(fā)生兩件及以上的事情。
線程并不是越多越好,每個(gè)線程都需要一個(gè)獨(dú)立的堆??臻g,線程切換也會(huì)耗費(fèi)時(shí)間。
并行:
detach():
未完待續(xù)
總結(jié)
以上是生活随笔為你收集整理的C++多线程详细讲解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。