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