[转]C++ 11 多线程--线程管理
轉(zhuǎn)載地址:https://www.cnblogs.com/wangguchangqing/p/6134635.html
說到多線程編程,那么就不得不提并行和并發(fā),多線程是實(shí)現(xiàn)并發(fā)(并行)的一種手段。并行是指兩個(gè)或多個(gè)獨(dú)立的操作同時(shí)進(jìn)行。注意這里是同時(shí)進(jìn)行,區(qū)別于并發(fā),在一個(gè)時(shí)間段內(nèi)執(zhí)行多個(gè)操作。在單核時(shí)代,多個(gè)線程是并發(fā)的,在一個(gè)時(shí)間段內(nèi)輪流執(zhí)行;在多核時(shí)代,多個(gè)線程可以實(shí)現(xiàn)真正的并行,在多核上真正獨(dú)立的并行執(zhí)行。例如現(xiàn)在常見的4核4線程可以并行4個(gè)線程;4核8線程則使用了超線程技術(shù),把一個(gè)物理核模擬為2個(gè)邏輯核心,可以并行8個(gè)線程。
并發(fā)編程的方法
通常,要實(shí)現(xiàn)并發(fā)有兩種方法:多進(jìn)程和多線程。
多進(jìn)程并發(fā)
使用多進(jìn)程并發(fā)是將一個(gè)應(yīng)用程序劃分為多個(gè)獨(dú)立的進(jìn)程(每個(gè)進(jìn)程只有一個(gè)線程),這些獨(dú)立的進(jìn)程間可以互相通信,共同完成任務(wù)。由于操作系統(tǒng)對進(jìn)程提供了大量的保護(hù)機(jī)制,以避免一個(gè)進(jìn)程修改了另一個(gè)進(jìn)程的數(shù)據(jù),使用多進(jìn)程比多線程更容易寫出安全的代碼。但這也造就了多進(jìn)程并發(fā)的兩個(gè)缺點(diǎn):
- 在進(jìn)程件的通信,無論是使用信號、套接字,還是文件、管道等方式,其使用要么比較復(fù)雜,要么就是速度較慢或者兩者兼而有之。
- 運(yùn)行多個(gè)線程的開銷很大,操作系統(tǒng)要分配很多的資源來對這些進(jìn)程進(jìn)行管理。
由于多個(gè)進(jìn)程并發(fā)完成同一個(gè)任務(wù)時(shí),不可避免的是:操作同一個(gè)數(shù)據(jù)和進(jìn)程間的相互通信,上述的兩個(gè)缺點(diǎn)也就決定了多進(jìn)程的并發(fā)不是一個(gè)好的選擇。
多線程并發(fā)
多線程并發(fā)指的是在同一個(gè)進(jìn)程中執(zhí)行多個(gè)線程。有操作系統(tǒng)相關(guān)知識(shí)的應(yīng)該知道,線程是輕量級的進(jìn)程,每個(gè)線程可以獨(dú)立的運(yùn)行不同的指令序列,但是線程不獨(dú)立的擁有資源,依賴于創(chuàng)建它的進(jìn)程而存在。也就是說,同一進(jìn)程中的多個(gè)線程共享相同的地址空間,可以訪問進(jìn)程中的大部分?jǐn)?shù)據(jù),指針和引用可以在線程間進(jìn)行傳遞。這樣,同一進(jìn)程內(nèi)的多個(gè)線程能夠很方便的進(jìn)行數(shù)據(jù)共享以及通信,也就比進(jìn)程更適用于并發(fā)操作。由于缺少操作系統(tǒng)提供的保護(hù)機(jī)制,在多線程共享數(shù)據(jù)及通信時(shí),就需要程序員做更多的工作以保證對共享數(shù)據(jù)段的操作是以預(yù)想的操作順序進(jìn)行的,并且要極力的避免死鎖(deadlock)。
C++ 11的多線程初體驗(yàn)
C++11的標(biāo)準(zhǔn)庫中提供了多線程庫,使用時(shí)需要#include <thread>頭文件,該頭文件主要包含了對線程的管理類std::thread以及其他管理線程相關(guān)的類。下面是使用C++多線程庫的一個(gè)簡單示例:
在一個(gè)for循環(huán)內(nèi),創(chuàng)建4個(gè)線程分別輸出數(shù)字0、1、2、3,并且在每個(gè)數(shù)字的末尾輸出換行符。語句thread t(output, i)創(chuàng)建一個(gè)線程t,該線程運(yùn)行output,第二個(gè)參數(shù)i是傳遞給output的參數(shù)。t在創(chuàng)建完成后自動(dòng)啟動(dòng),t.detach表示該線程在后臺(tái)允許,無需等待該線程完成,繼續(xù)執(zhí)行后面的語句。這段代碼的功能是很簡單的,如果是順序執(zhí)行的話,其結(jié)果很容易預(yù)測得到
0 \n 1 \n 2 \n 3 \n但是在并行多線程下,其執(zhí)行的結(jié)果就多種多樣了,下圖是代碼一次運(yùn)行的結(jié)果:
可以看出,首先輸出了01,并沒有輸出換行符;緊接著卻連續(xù)輸出了2個(gè)換行符。不是說好的并行么,同時(shí)執(zhí)行,怎么還有先后的順序?這就涉及到多線程編程最核心的問題了資源競爭。CPU有4核,可以同時(shí)執(zhí)行4個(gè)線程這是沒有問題了,但是控制臺(tái)卻只有一個(gè),同時(shí)只能有一個(gè)線程擁有這個(gè)唯一的控制臺(tái),將數(shù)字輸出。將上面代碼創(chuàng)建的四個(gè)線程進(jìn)行編號:t0,t1,t2,t3,分別輸出的數(shù)字:0,1,2,3。參照上圖的執(zhí)行結(jié)果,控制臺(tái)的擁有權(quán)的轉(zhuǎn)移如下:
- t0擁有控制臺(tái),輸出了數(shù)字0,但是其沒有來的及輸出換行符,控制的擁有權(quán)卻轉(zhuǎn)移到了t1;(0)
- t1完成自己的輸出,t1線程完成 (1\n)
- 控制臺(tái)擁有權(quán)轉(zhuǎn)移給t0,輸出換行符 (\n)
- t2擁有控制臺(tái),完成輸出 (2\n)
- t3擁有控制臺(tái),完成輸出 (3\n)
由于控制臺(tái)是系統(tǒng)資源,這里控制臺(tái)擁有權(quán)的管理是操作系統(tǒng)完成的。但是,假如是多個(gè)線程共享進(jìn)程空間的數(shù)據(jù),這就需要自己寫代碼控制,每個(gè)線程何時(shí)能夠擁有共享數(shù)據(jù)進(jìn)行操作。共享數(shù)據(jù)的管理以及線程間的通信,是多線程編程的兩大核心。
線程管理
每個(gè)應(yīng)用程序至少有一個(gè)進(jìn)程,而每個(gè)進(jìn)程至少有一個(gè)主線程,除了主線程外,在一個(gè)進(jìn)程中還可以創(chuàng)建多個(gè)線程。每個(gè)線程都需要一個(gè)入口函數(shù),入口函數(shù)返回退出,該線程也會(huì)退出,主線程就是以main函數(shù)作為入口函數(shù)的線程。在C++ 11的線程庫中,將線程的管理在了類std::thread中,使用std::thread可以創(chuàng)建、啟動(dòng)一個(gè)線程,并可以將線程掛起、結(jié)束等操作。
啟動(dòng)一個(gè)線程
C++ 11的線程庫啟動(dòng)一個(gè)線程是非常簡單的,只需要?jiǎng)?chuàng)建一個(gè)std::thread對象,就會(huì)啟動(dòng)一個(gè)線程,并使用該std::thread對象來管理該線程。
do_task(); std::thread(do_task);這里創(chuàng)建std::thread傳入的函數(shù),實(shí)際上其構(gòu)造函數(shù)需要的是可調(diào)用(callable)類型,只要是有函數(shù)調(diào)用類型的實(shí)例都是可以的。所有除了傳遞函數(shù)外,還可以使用:
- lambda表達(dá)式
使用lambda表達(dá)式啟動(dòng)線程輸出數(shù)字
for (int i = 0; i < 4; i++) {thread t([i]{ cout << i << endl; }); t.detach(); }- 重載了()運(yùn)算符的類的實(shí)例
使用重載了()運(yùn)算符的類實(shí)現(xiàn)多線程數(shù)字輸出
class Task { public:void operator()(int i) { cout << i << endl; } }; int main() { for (uint8_t i = 0; i < 4; i++) { Task task; thread t(task, i); t.detach(); } }把函數(shù)對象傳入std::thread的構(gòu)造函數(shù)時(shí),要注意一個(gè)C++的語法解析錯(cuò)誤(C++'s most vexing parse)。向std::thread的構(gòu)造函數(shù)中傳入的是一個(gè)臨時(shí)變量,而不是命名變量就會(huì)出現(xiàn)語法解析錯(cuò)誤。如下代碼:
std::thread t(Task());這里相當(dāng)于聲明了一個(gè)函數(shù)t,其返回類型為thread,而不是啟動(dòng)了一個(gè)新的線程。可以使用新的初始化語法避免這種情況
std::thread t{Task()};當(dāng)線程啟動(dòng)后,一定要在和線程相關(guān)聯(lián)的thread銷毀前,確定以何種方式等待線程執(zhí)行結(jié)束。C++11有兩種方式來等待線程結(jié)束
- detach方式,啟動(dòng)的線程自主在后臺(tái)運(yùn)行,當(dāng)前的代碼繼續(xù)往下執(zhí)行,不等待新線程結(jié)束。前面代碼所使用的就是這種方式。
- join方式,等待啟動(dòng)的線程完成,才會(huì)繼續(xù)往下執(zhí)行。假如前面的代碼使用這種方式,其輸出就會(huì)0,1,2,3,因?yàn)槊看味际乔耙粋€(gè)線程輸出完成了才會(huì)進(jìn)行下一個(gè)循環(huán),啟動(dòng)下一個(gè)新線程。
無論在何種情形,一定要在thread銷毀前,調(diào)用t.join或者t.detach,來決定線程以何種方式運(yùn)行。當(dāng)使用join方式時(shí),會(huì)阻塞當(dāng)前代碼,等待線程完成退出后,才會(huì)繼續(xù)向下執(zhí)行;而使用detach方式則不會(huì)對當(dāng)前代碼造成影響,當(dāng)前代碼繼續(xù)向下執(zhí)行,創(chuàng)建的新線程同時(shí)并發(fā)執(zhí)行,這時(shí)候需要特別注意:創(chuàng)建的新線程對當(dāng)前作用域的變量的使用,創(chuàng)建新線程的作用域結(jié)束后,有可能線程仍然在執(zhí)行,這時(shí)局部變量隨著作用域的完成都已銷毀,如果線程繼續(xù)使用局部變量的引用或者指針,會(huì)出現(xiàn)意想不到的錯(cuò)誤,并且這種錯(cuò)誤很難排查。例如:
auto fn = [](int *a){for (int i = 0; i < 10; i++) cout << *a << endl; }; []{ int a = 100; thread t(fn, &a); t.detach(); }();在lambda表達(dá)式中,使用fn啟動(dòng)了一個(gè)新的線程,在裝個(gè)新的線程中使用了局部變量a的指針,并且將該線程的運(yùn)行方式設(shè)置為detach。這樣,在lamb表達(dá)式執(zhí)行結(jié)束后,變量a被銷毀,但是在后臺(tái)運(yùn)行的線程仍然在使用已銷毀變量a的指針,其輸出結(jié)果如下:
只有第一個(gè)輸出是正確的值,后面輸出的值是a已被銷毀后輸出的結(jié)果。所以在以detach的方式執(zhí)行線程時(shí),要將線程訪問的局部數(shù)據(jù)復(fù)制到線程的空間(使用值傳遞),一定要確保線程沒有使用局部變量的引用或者指針,除非你能肯定該線程會(huì)在局部作用域結(jié)束前執(zhí)行結(jié)束。當(dāng)然,使用join方式的話就不會(huì)出現(xiàn)這種問題,它會(huì)在作用域結(jié)束前完成退出。
異常情況下等待線程完成
當(dāng)決定以detach方式讓線程在后臺(tái)運(yùn)行時(shí),可以在創(chuàng)建thread的實(shí)例后立即調(diào)用detach,這樣線程就會(huì)后thread的實(shí)例分離,即使出現(xiàn)了異常thread的實(shí)例被銷毀,仍然能保證線程在后臺(tái)運(yùn)行。但線程以join方式運(yùn)行時(shí),需要在主線程的合適位置調(diào)用join方法,如果調(diào)用join前出現(xiàn)了異常,thread被銷毀,線程就會(huì)被異常所終結(jié)。為了避免異常將線程終結(jié),或者由于某些原因,例如線程訪問了局部變量,就要保證線程一定要在函數(shù)退出前完成,就要保證要在函數(shù)退出前調(diào)用join
void func() {thread t([]{ cout << "hello C++ 11" << endl; }); try { do_something_else(); } catch (...) { t.join(); throw; } t.join(); }上面代碼能夠保證在正常或者異常的情況下,都會(huì)調(diào)用join方法,這樣線程一定會(huì)在函數(shù)func退出前完成。但是使用這種方法,不但代碼冗長,而且會(huì)出現(xiàn)一些作用域的問題,并不是一個(gè)很好的解決方法。
一種比較好的方法是資源獲取即初始化(RAII,Resource Acquisition Is Initialization),該方法提供一個(gè)類,在析構(gòu)函數(shù)中調(diào)用join。
class thread_guard {thread &t; public :explicit thread_guard(thread& _t) : t(_t){} ~thread_guard() { if (t.joinable()) t.join(); } thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete; }; void func(){ thread t([]{ cout << "Hello thread" <<endl ; }); thread_guard g(t); }無論是何種情況,當(dāng)函數(shù)退出時(shí),局部變量g調(diào)用其析構(gòu)函數(shù)銷毀,從而能夠保證join一定會(huì)被調(diào)用。
向線程傳遞參數(shù)
向線程調(diào)用的函數(shù)傳遞參數(shù)也是很簡單的,只需要在構(gòu)造thread的實(shí)例時(shí),依次傳入即可。例如:
void func(int *a,int n){} int buffer[10]; thread t(func,buffer,10); t.join();需要注意的是,默認(rèn)的會(huì)將傳遞的參數(shù)以拷貝的方式復(fù)制到線程空間,即使參數(shù)的類型是引用。例如:
void func(int a,const string& str); thread t(func,3,"hello");func的第二個(gè)參數(shù)是string &,而傳入的是一個(gè)字符串字面量。該字面量以const char*類型傳入線程空間后,在線程的空間內(nèi)轉(zhuǎn)換為string。
如果在線程中使用引用來更新對象時(shí),就需要注意了。默認(rèn)的是將對象拷貝到線程空間,其引用的是拷貝的線程空間的對象,而不是初始希望改變的對象。如下:
class _tagNode { public:int a;int b; };void func(_tagNode &node) { node.a = 10; node.b = 20; } void f() { _tagNode node; thread t(func, node); t.join(); cout << node.a << endl ; cout << node.b << endl ; }在線程內(nèi),將對象的字段a和b設(shè)置為新的值,但是在線程調(diào)用結(jié)束后,這兩個(gè)字段的值并不會(huì)改變。這樣由于引用的實(shí)際上是局部變量node的一個(gè)拷貝,而不是node本身。在將對象傳入線程的時(shí)候,調(diào)用std::ref,將node的引用傳入線程,而不是一個(gè)拷貝。thread t(func,std::ref(node));
也可以使用類的成員函數(shù)作為線程函數(shù),示例如下
class _tagNode{public:void do_some_work(int a); }; _tagNode node; thread t(&_tagNode::do_some_work, &node,20);上面創(chuàng)建的線程會(huì)調(diào)用node.do_some_work(20),第三個(gè)參數(shù)為成員函數(shù)的第一個(gè)參數(shù),以此類推。
轉(zhuǎn)移線程的所有權(quán)
thread是可移動(dòng)的(movable)的,但不可復(fù)制(copyable)。可以通過move來改變線程的所有權(quán),靈活的決定線程在什么時(shí)候join或者detach。
thread t1(f1); thread t3(move(t1));將線程從t1轉(zhuǎn)移給t3,這時(shí)候t1就不再擁有線程的所有權(quán),調(diào)用t1.join或t1.detach會(huì)出現(xiàn)異常,要使用t3來管理線程。這也就意味著thread可以作為函數(shù)的返回類型,或者作為參數(shù)傳遞給函數(shù),能夠更為方便的管理線程。
線程的標(biāo)識(shí)類型為std::thread::id,有兩種方式獲得到線程的id。
- 通過thread的實(shí)例調(diào)用get_id()直接獲取
- 在當(dāng)前線程上調(diào)用this_thread::get_id()獲取
總結(jié)
本文主要介紹了C++11引入的標(biāo)準(zhǔn)多線程庫的一些基本操作。有以下內(nèi)容:
- 線程的創(chuàng)建
- 線程的執(zhí)行方式,join或者detach
- 向線程函數(shù)傳遞參數(shù),需要注意的是線程默認(rèn)是以拷貝的方式傳遞參數(shù)的,當(dāng)期望傳入一個(gè)引用時(shí),要使用std::ref進(jìn)行轉(zhuǎn)換
- 線程是movable的,可以在函數(shù)內(nèi)部或者外部進(jìn)行傳遞
- 每個(gè)線程都一個(gè)標(biāo)識(shí),可以調(diào)用get_id獲取。
如果您覺得閱讀本文對您有幫助,請點(diǎn)一下“推薦”按鈕,您的“推薦”將是我最大的寫作動(dòng)力!歡迎各位轉(zhuǎn)載,但是未經(jīng)作者本人同意,轉(zhuǎn)載文章之后必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責(zé)任的權(quán)利。
轉(zhuǎn)載于:https://www.cnblogs.com/qiuheng/p/9274008.html
總結(jié)
以上是生活随笔為你收集整理的[转]C++ 11 多线程--线程管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 提高Python运行效率的6大技巧!
- 下一篇: C++ Primer plus 第12章