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