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