C++11學(xué)習(xí)之多線程的支持詳解
C++11中的多線程的支持
千禧年以后,主流的芯片廠商都開始生產(chǎn)多核處理器,所以并行編程越來越重要了。在C++98中根本沒有自己的一套多線程編程庫,它采用的是C99中的POSIX標(biāo)準(zhǔn)的pthread庫中的互斥鎖,來完成多線程編程。
首先來簡單一個(gè)概念:原子操作,即多線程程序中"最小的且不可以并行化的操作"。通俗來說,如果對一個(gè)資源的操作是原子操作,就意味著一次性只有一個(gè)線程的一個(gè)原子操作可以對這個(gè)資源進(jìn)行操作。在C99中,我們一般都是采用互斥鎖來完成粗粒度的原子操作。
#include<pthread.h> #include<iostream> using namespace std; static long long total =0; pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;//互斥鎖 void * func(void *) { long long i; for(i=0;i<100000000LL;i++) { pthread_mutex_lock(&m); total +=i; pthread_mutex_unlock(&m); } } int main() { pthread_t thread1,thread2; if(pthread_create(&thread1,nullptr,&func,nullptr)) { throw; } if(pthread_create(&thread2,nullptr,&func,nullptr)) { throw; } pthread_join(thread1,nullptr); pthread_join(thread2,nullptr); cout<<total<<endl;//9999999900000000 }
可以看出來,上書代碼中total +=i;就是原子操作。
1.C++11中的原子類型
我們發(fā)現(xiàn),在C99中的互斥鎖需要顯式聲明,要自己開關(guān)鎖,為了簡化代碼,C++11中定義了原子類型。這些原子類型是一個(gè)class,它們的接口都是原子操作。如下所示:
#include<atomic> #include<thread> #include<iostream> using namespace std; atomic_llong total {0};//原子數(shù)據(jù)類型 void func(int) { for(long long i=0;i<100000000LL;i++) { total+=i; } } int main() { thread t1(func,0); thread t2(func,0); t1.join(); t2.join(); cout<<total<<endl;//9999999900000000 }
上述代碼中total就是一個(gè)原子類對象,它的接口例如這里的重載operator+=()就是一個(gè)原子操作,所以我們不需要顯式調(diào)用互斥鎖了。
總共有多少原子類型呢?C++11的做法是,存在一個(gè)atomic類模板,我們可以通過這個(gè)類模板定義出想要的原子類型:
using atomic_llong = atomic<long long>;
所以我們想把什么類型搞成原子類型,只需要傳入不同的模板實(shí)參就行了。
總之,C++11中原子操作就是atomic模板類的成員函數(shù)。
1.1 原子類型的接口
我們知道原子類型的接口就是原子操作,但是我們現(xiàn)在關(guān)注一下,它們有哪些接口?
原子類型屬于資源類數(shù)據(jù),多個(gè)線程只能訪問單個(gè)預(yù)祝你類型的拷貝。所以C++11中的原子類型不支持移動(dòng)語義和拷貝語義,原子類型的操作都是對那個(gè)唯一的一份資源操作的,原子類型沒有拷貝構(gòu)造,拷貝賦值,移動(dòng)構(gòu)造和移動(dòng)賦值的。
atomic<float> af{1.2f};//正確 atomic<float> af1{af};//錯(cuò)誤,原子類型不支持拷貝語義 float f=af;//正確,調(diào)用了原子類型的接口 af=0.0;//正確,調(diào)用了原子類型的接口
看一下上表中的一些原子類型的接口,load()是進(jìn)行讀取操作的,例如
atomic<int> a(2); int b=a; b=a.load();
上如代碼中的,b=a就是等價(jià)于b=a.load(),實(shí)際上,atomic<int>中存在operator int()接口,這個(gè)接口中:
operator __int_type() const noexcept { return load(); }
store()接口是用來寫數(shù)據(jù)的的:
atomic<int> a; a=1; a.store(1);
上述代碼中a=1相當(dāng)于a.load(1),atomic<int>中存在operator=(int)接口,它的實(shí)現(xiàn)如下:
__int_type operator=(__int_type __i) noexcept { store(__i); return __i; }
例如其他操作,比如exchange是做交換,compare_exchange_weak/strong()是比較并交換(CAS操作的),它們的實(shí)現(xiàn)會(huì)更復(fù)雜一些,還有一些符號的重載,這里就不一一介紹了,<<C++ Concurrency in Action>>的第5章和第7章會(huì)詳細(xì)介紹這部分內(nèi)容。
值得注意的是,這里有一個(gè)特殊的原子類型atomic_flag,這是一個(gè)原子類型是無鎖的,也就是說線程對這種類型的數(shù)據(jù)的訪問是無鎖的,所以它就不需要接口:load和store,即多個(gè)線程可以同時(shí)操作這個(gè)資源。我們可以用它來實(shí)現(xiàn)自旋鎖
1.2簡單自旋鎖的實(shí)現(xiàn)
互斥鎖是說,當(dāng)一個(gè)線程訪問一個(gè)資源的時(shí)候,他會(huì)給進(jìn)入臨界區(qū)代碼設(shè)置一把鎖,出來的時(shí)候就把鎖給打開,當(dāng)其他進(jìn)程要進(jìn)入臨界區(qū)的時(shí)候,就會(huì)實(shí)現(xiàn)看一下鎖,如果鎖是關(guān)的,那就阻塞自己,這樣core就會(huì)去執(zhí)行其他工作,導(dǎo)致上下文切換嗎,所以它的效率會(huì)比較低。原子類型中is_lock_free()就是說明的這個(gè)原子類型的訪問是否使用的互斥鎖。
與互斥鎖相反的是自旋鎖,區(qū)別是,當(dāng)其他進(jìn)程要進(jìn)入臨界區(qū)的時(shí)候,如果鎖是關(guān)的,它不會(huì)阻塞自己,而是不斷查看鎖是不是開的,這樣就不會(huì)引發(fā)上下文切換,但是同樣也會(huì)增加cpu利用率。
我們可以使用atomic_flag原子類型來實(shí)現(xiàn)自旋鎖,因?yàn)樵赼tomic_flag本身是無鎖的,所以多個(gè)線程可以同時(shí)訪問它,相當(dāng)于同時(shí)訪問這把自旋鎖,實(shí)現(xiàn)如下:
#include<thread> #include<atomic> #include<iostream> #include<unistd.h> using namespace std; atomic_flag lock=ATOMIC_FLAG_INIT;//獲得自旋鎖 void f(int n) { while(lock.test_and_set()) {//嘗試獲得原子鎖 cout<<"Waiting from thread "<<n<<endl; } cout<<"Thread "<<n<<" starts working"<<endl; } void g(int n) { cout<<"Thread "<<n<<" is going to start"<<endl; lock.clear();//打開鎖 cout<<"Thread "<<n<<" starts working"<<endl; } int main() { lock.test_and_set();//關(guān)上鎖 thread t1(f,1); thread t2(g,2); t1.join(); usleep(100000); t2.join(); }
這里的test_and_set()是一個(gè)原子操作,它做的是,寫入新值并返回舊值。在main()中,我們首先給這個(gè)lock變量,寫入true值,即關(guān)上鎖,然后再線程t1中,它不斷嘗試獲得自旋鎖,再線程t2中,clear()接口,相當(dāng)于將lock變量值變成false,這時(shí)自旋鎖就打開了,這樣子,線程t1就可以執(zhí)行剩下的代碼了。
簡單的我們可以將lock封裝一下
void Lock(atomic_flag & lock) { while(lock.test_and_set()); } void Unlock(atomic_flag & lock) { lock.clear(); }
上面操作中,我們就相當(dāng)于完成了一把鎖,可以用其實(shí)現(xiàn)互斥訪問臨界區(qū)的功能了。不過這個(gè)和C99中的pthread_mutex_lock()和pthread_mutex_unlock()不一樣,C99中的這兩個(gè)鎖是互斥鎖,而上面代碼中實(shí)現(xiàn)的是自旋鎖。
2.提高并行程度
#include <thread> #include <atomic> atomic<int> a; atomic<int> b; void threadHandle() { int t = 1; a = t; b = 2; // b 的賦值不依賴 a }
在上面代碼中,對a和b的賦值語句實(shí)際上可以不管先后的,如果允許編譯器或者硬件對其重排序或者并發(fā)執(zhí)行,那就會(huì)提高并行程度。
在單線程程序中,我們根部不關(guān)心它們的執(zhí)行順序,反正結(jié)果都是一樣的,但是多線程不一樣,如果執(zhí)行順序不一樣,結(jié)果就會(huì)不同。
#include <thread> #include <atomic> #include<iostream> using namespace std; atomic<int> a{0}; atomic<int> b{0}; void ValueSet(int ) { int t = 1; a = t; b = 2; // b 的賦值不依賴 a } int Observer(int) { cout<<"("<<a<<","<<b<<")"<<endl; } int main() { thread t1(ValueSet,0); thread t2(Observer,0); t1.join(); t2.join(); cout<<"Final: ("<<a<<","<<b<<")"<<endl; }
上面代碼中,Observer()中的輸出結(jié)果,會(huì)和a和b的賦值順序有關(guān),它的輸出結(jié)果肯可能是:(0,0) (1,0) (0,2) (1,2)。這就說明了,多線程程序中,如果執(zhí)指令行順序不一樣,結(jié)果就會(huì)不同。
影響并行程度的兩個(gè)關(guān)鍵因素是:編譯器是否有權(quán)對指令進(jìn)行重排序和硬件是否有權(quán)對匯編代碼重排序。
C++11中,我們可以顯式得告訴編譯器和硬件它們的權(quán)限,進(jìn)而提高并發(fā)程度。通俗來說,如果我們要求并行程度最高,那么我們就授權(quán)給編譯器和硬件,允許它們重排序指令。
2.1 memory_order的參數(shù)
原子類型的成員函數(shù)中,大多數(shù)都可以接收一個(gè)類型為memory_order的參數(shù),它就是可以告訴編譯器和硬件,是否可以重排序。
typedef enum memory_order { memory_order_relaxed, // 不對執(zhí)行順序做保證 memory_order_acquire, // 本線程中,所有后續(xù)的讀操作必須在本條原子操作完成后執(zhí)行 memory_order_release, // 本線程中,所有之前的寫操作完成后才能執(zhí)行本條原子操作 memory_order_acq_rel, // 同時(shí)包含 memory_order_acquire 和 memory_order_release memory_order_consume, // 本線程中,所有后續(xù)的有關(guān)本原子類型的操作,必須在本條原子操作完成之后執(zhí)行 memory_order_seq_cst // 全部存取都按順序執(zhí)行 } memory_order;
在C++11中,memory_order的參數(shù)的默認(rèn)值是memory_order_seq_cst,即不允許編譯器和硬件進(jìn)行重排序,這樣一來,在上嗎代碼中的Observer()中輸出結(jié)果就不可能是(0,2),因?yàn)閷的賦值語句是先于b的。這實(shí)際上就是:順序一致性,準(zhǔn)確來說就是在同一個(gè)線程中,原子操作的順序和代碼的順序保持一致。
而如果我們改動(dòng)一下代碼:
#include <thread> #include <atomic> #include<iostream> using namespace std; atomic<int> a{0}; atomic<int> b{0}; void ValueSet(int ) { int t = 1; a.store(t,memory_order_relaxed); b.store(2,memory_order_relaxed); // b 的賦值不依賴 a } int Observer(int) { cout<<"("<<a<<","<<b<<")"<<endl; } int main() { thread t1(ValueSet,0); thread t2(Observer,0); t1.join(); t2.join(); cout<<"Final: ("<<a<<","<<b<<")"<<endl; }
在上面代碼中的Observer()中輸出結(jié)果是有可能是:(0,2)的,因?yàn)檫@里的memory_order_relaxed不對原子操作的順序有嚴(yán)格要求,就有可能發(fā)生b先被賦值了,而此時(shí)a還沒被賦值的情況。
所以,為了進(jìn)一步開發(fā)原子操作的并行程度,我們的目標(biāo)是:保證程序既快又對。
2.2 release-acquire內(nèi)存順序
#include <thread> #include <atomic> #include<iostream> using namespace std; atomic<int> a; atomic<int> b; void Thread1(int ) { int t = 1; a.store(t,memory_order_relaxed); b.store(2,memory_order_release); // 本操作前的寫操作必須先完成,即保證a的賦值快于b } void Thread2(int ) { while(b.load(memory_order_acquire)!=2);//必須等該原子操作完成后,才執(zhí)行下面代碼 cout<<a.load(memory_order_relaxed)<<endl;//1 } int main() { thread t1(Thread1,0); thread t2(Thread2,0); t2.join(); t1.join(); }
上面代碼中,實(shí)際上也是實(shí)現(xiàn)了一種自旋鎖的操作,我們保證了a.store快于b.store,而b.load又一定快于a.load。而且,對于b的store和load就實(shí)現(xiàn)了一種release-acquire內(nèi)存順序.
2.3 release-consume內(nèi)存順序
#include<thread> #include<atomic> #include<cassert> #include<string> using namespace std; atomic<string*> ptr; atomic<int> date; void Producer() { string *p=new string("hello"); date.store(42,memory_order_relaxed); ptr.store(p,memory_order_release);//date賦值快于ptr } void Consumer() { string *p2; while(!(p2=ptr.load(memory_order_consume))); assert(*p2=="hello");//一定成立 assert(date.load(memory_order_relaxed)==42);//可能斷言失敗,因?yàn)檫@個(gè)指令可能在本線程中首先執(zhí)行 } int main() { thread t1(Producer); thread t2(Consumer); t1.join(); t2.join(); }
上面的內(nèi)存順序也叫生產(chǎn)者-消費(fèi)者順序。
實(shí)際上,總共的內(nèi)存模型就是4個(gè):順序一致性,松散的(relaxed),release-consume和release-acquire。
2.4 小結(jié)
實(shí)際上,對于并行編程來說,最根本的的在于,并行算法,而不是從硬件上搞內(nèi)存模型優(yōu)化啥的,如果你嫌麻煩的話,全部使用順序一致性內(nèi)存模型,對并行效率的影響也不是很大。
3.線程局部存儲(chǔ)
線程擁有自己的??臻g,但是堆空間,靜態(tài)數(shù)據(jù)區(qū)(文件data,bss段,全局/靜態(tài)變量)是共享的。線程之間互相共享,靜態(tài)數(shù)據(jù)當(dāng)然是很好的,但是我們也需要線程自己的局部變量
#include<pthread.h> #include<iostream> using namespace std; int thread_local errorCode=0; void* MaySetErr(void *input) { if(*(int*)input==1) errorCode=1; else if(*(int*)input==2) errorCode=2; else errorCode=0; cout<<errorCode<<endl; } int main() { int input_a=1; int input_b=2; pthread_t thread1,thread2; pthread_create(&thread1,nullptr,&MaySetErr,&input_a); pthread_create(&thread2,nullptr,&MaySetErr,&input_b); pthread_join(thread1,nullptr); pthread_join(thread2,nullptr); cout<<errorCode<<endl;//0 }
上面代碼中的errorCode是一個(gè)thread_local變量,它意味著它是一個(gè)線程內(nèi)部的全局變量,線程開始時(shí),他會(huì)被初始化,然后線程結(jié)束時(shí),該值就不會(huì)有效。實(shí)際上兩個(gè)進(jìn)程中會(huì)有各自的errorCode,而main函數(shù)中也有自己的errorCode
4.快速退出
在C++98中,我們會(huì)見到3中終止函數(shù):terminate,abort,exit。而在C++11中我們增加了quick_exit終止函數(shù),這種終止函數(shù)主要用在線程種。
1.terminate函數(shù),它是C++種的異常機(jī)制有關(guān)的,通常沒有被捕獲的異常就會(huì)調(diào)用terminate
2.abort函數(shù)是底層的終止函數(shù),terminate就是調(diào)用它來終止進(jìn)程的,但是abort調(diào)用時(shí),不會(huì)調(diào)用任何析構(gòu)函數(shù),會(huì)引發(fā)內(nèi)存泄漏啥的,但是一般來說,他會(huì)給符合POSIX的操作系統(tǒng)拋出一個(gè)信號,此時(shí)signal handler就會(huì)默認(rèn)的釋放進(jìn)程種的所有資源,來避免內(nèi)存泄漏。
3.exit函數(shù)是正常退出,他會(huì)調(diào)用析構(gòu)函數(shù),但是有時(shí)候析構(gòu)函數(shù)狠復(fù)雜,那我們還不如直接調(diào)用absort函數(shù),將釋放資源的事情留給操作系統(tǒng)。
在多線程情況下,我們一般都是采用exit來退出的,但是這樣容易卡死,當(dāng)線程復(fù)雜的時(shí)候,exit這種正常退出方式,太過于保守了,但是abort這種退出方式又太激進(jìn)了,所以有一種新的退出函數(shù):quick_exit函數(shù)。
quick_exit
這個(gè)函數(shù)不執(zhí)行析構(gòu)函數(shù),而使得程序終止,但是和abort不同的是,abort一般都是異常退出,而quick_exit是正常退出。
#include<cstdlib> #include<iostream> using namespace std; struct A{~A(){cout<<"Destruct A."<<endl;}}; void closeDevice(){cout<<"device is closed."<<endl;} int main() { A a; at_quick_exit(closeDevice); quick_exit(0); } //樣容易卡死,當(dāng)線程復(fù)雜的時(shí)候,`exit`這種正常退出方式,太過于保守了,但是`abort`這種退出方式又太激進(jìn)了,所以有一種新的退出函數(shù):`quick_exit`函數(shù)。
//這個(gè)函數(shù)不執(zhí)行析構(gòu)函數(shù),而使得程序終止,但是和`abort`不同的是,`abort`一般都是異常退出,而`quick_exit`是正常退出。 #include<cstdlib> #include<iostream> using namespace std; struct A{~A(){cout<<"Destruct A."<<endl;}}; void closeDevice(){cout<<"device is closed."<<endl;} int main() { A a; at_quick_exit(closeDevice); quick_exit(0); }
以上就是C++11學(xué)習(xí)之多線程的支持詳解的詳細(xì)內(nèi)容,更多關(guān)于C++11多線程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Qt專欄之模態(tài)與非模態(tài)對話框的實(shí)現(xiàn)
這篇文章主要介紹了Qt專欄之模態(tài)與非模態(tài)對話框的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Qt實(shí)現(xiàn)手動(dòng)切換多種布局的完美方案
通過點(diǎn)擊程序界面上不同的布局按鈕,使主工作區(qū)呈現(xiàn)出不同的頁面布局,多個(gè)布局之間可以通過點(diǎn)擊不同布局按鈕切換,支持的最多的窗口為9個(gè),不同布局下窗口數(shù)隨之變化,這篇文章主要介紹了Qt實(shí)現(xiàn)手動(dòng)切換多種布局的完美方案,需要的朋友可以參考下2024-07-07C++11 線程同步接口std::condition_variable和std::future的簡單使用示例詳
本文介紹了std::condition_variable和std::future在C++中的應(yīng)用,用于線程間的同步和異步執(zhí)行,通過示例代碼,展示了如何使用std::condition_variable的wait和notify接口進(jìn)行線程間同步2024-09-09C++中回調(diào)函數(shù)及函數(shù)指針的實(shí)例詳解
這篇文章主要介紹了C++中回調(diào)函數(shù)及函數(shù)指針的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10