詳解C++11中的線程庫
一、線程庫的介紹
在C++11之前,涉及到多線程問題,都是和平臺相關(guān)的,比如windows和linux下各有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進(jìn)行支持了,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。要使用標(biāo)準(zhǔn)庫中的線程,必須包含< thread >頭文件。
特點:跨平臺、面向?qū)ο蠓庋b的類(每個線程是個類對象)。
其實現(xiàn)原理是封裝庫時使用了條件編譯,也就是說他的底層還是分別調(diào)用了不同平臺的線程API。
函數(shù)名 | 功能 |
---|---|
thread() | 構(gòu)造一個線程對象,沒有關(guān)聯(lián)任何線程函數(shù),即沒有啟動任何線程 |
thread(fn,args1, args2,…) | 構(gòu)造一個線程對象,并關(guān)聯(lián)線程函數(shù)fn,args1,args2,…為線程函數(shù)的參數(shù) |
get_id() | 獲取線程id |
jionable() | 線程是否還在執(zhí)行,joinable代表的是一個正在執(zhí)行中的線程。 |
jion() | 該函數(shù)調(diào)用后會阻塞住線程,當(dāng)該線程結(jié)束后,主線程繼續(xù)執(zhí)行 |
detach() | 在創(chuàng)建線程對象后馬上調(diào)用,用于把被創(chuàng)建線程與線程對象分離開,分離的線程變?yōu)楹笈_線程,創(chuàng)建的線程的"死活"就與主線程無關(guān) |
1.1. 使用時的注意點
線程是操作系統(tǒng)中的一個概念,線程對象可以關(guān)聯(lián)一個線程,用來控制線程以及獲取線程的狀態(tài)
當(dāng)創(chuàng)建一個線程對象后,沒有提供線程函數(shù),該對象實際沒有對應(yīng)任何線程。
#include <thread> int main() { std::thread t1; cout << t1.get_id() << endl;//0 return 0; }
get_id()的返回值類型為id類型,id類型實際為std::thread命名空間下封裝的一個類,該類中包含了一個結(jié)構(gòu)體:
// vs下查看 typedef struct { /* thread identifier for Win32 */ void *_Hnd; /* Win32 HANDLE */ unsigned int _Id; } _Thrd_imp_t;
當(dāng)創(chuàng)建一個線程對象后,并且給線程關(guān)聯(lián)線程函數(shù),該線程就被啟動,與主線程一起運(yùn)行。線程函數(shù)一般情況下可按照以下三種方式提供:函數(shù)指針、lambda表達(dá)式、函數(shù)對象
#include <iostream> using namespace std; #include <thread> void ThreadFunc(int a) { cout << "Thread1" << a << endl; } class TF { public: void operator()() { cout << "Thread3" << endl; } }; int main() // 線程函數(shù)為函數(shù)指針 thread t1(ThreadFunc, 10); // 線程函數(shù)為lambda表達(dá)式 thread t2([]{cout << "Thread2" << endl; }); // 線程函數(shù)為函數(shù)對象 TF tf; thread t3(tf); t1.join(); t2.join(); t3.join(); cout << "Main thread!" << endl; return 0; }
4.thread類是防拷貝的,不允許拷貝構(gòu)造以及賦值,但是可以移動構(gòu)造和移動賦值,即將一個線程對象關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移給其他線程對象,轉(zhuǎn)移期間不意向線程的執(zhí)行
5.可以通過jionable()函數(shù)判斷線程是否是有效的,如果是以下任意情況,則線程無效:
- 采用無參構(gòu)造函數(shù)構(gòu)造的線程對象(沒有提供線程函數(shù),即這個線程本身就是無效的)
- 線程對象的狀態(tài)已經(jīng)轉(zhuǎn)移給其他線程對象(資源被轉(zhuǎn)移)
- 線程已經(jīng)調(diào)用jion或者detach結(jié)束(線程已結(jié)束,或者已經(jīng)分離)
1.2. 線程函數(shù)參數(shù)
線程函數(shù)的參數(shù)是以值拷貝的方式拷貝到線程棧空間中的,因此:即使線程參數(shù)為引用類型,在線程中修改后也不能修改外部實參,因為其實際引用的是線程棧中的拷貝,而不是外部實參。
注意:如果是類成員函數(shù)作為線程參數(shù)時,必須將this作為線程函數(shù)參數(shù)
1.3. join與detach
啟動了一個線程后,當(dāng)這個線程結(jié)束的時候,如何去回收線程所使用的資源呢?thread庫給我們兩種選擇:
join()方式
join():主線程被阻塞,當(dāng)新線程終止時,join()會清理相關(guān)的線程資源,然后返回,主線程再繼續(xù)向下執(zhí)行,然后銷毀線程對象。由于join()清理了線程的相關(guān)資源,thread對象與已銷毀的線程就沒有關(guān)系了,因此一個線程對象只能使用一次join(),否則程序會崩潰。
采用jion()方式結(jié)束線程時,jion()的調(diào)用位置非常關(guān)鍵。為了避免該問題,可以采用RAII的方式對線程對象進(jìn)行封裝,RAII :利用對象聲明周期來管理線程資源,保證線程資源正常銷毀,資源獲取立即初始化,在構(gòu)造函數(shù)中初始化資源,在析構(gòu)函數(shù)中銷毀資源。 比如:
#include <thread> class mythread { public: explicit mythread(std::thread &t) :m_t(t){} ~mythread() { if (m_t.joinable())//判斷線程是否還在 m_t.join(); } mythread(mythread const&)=delete; mythread& operator=(const mythread &)=delete; private: std::thread &m_t; }; void ThreadFunc() { cout << "ThreadFunc()" << endl; } bool DoSomething() { return false; } int main() { thread t(ThreadFunc); mythread q(t); if (DoSomething()) return -1; return 0; }
detach()方式
detach():該函數(shù)被調(diào)用后,新線程與線程對象分離,不再被線程對象所表達(dá),就不能通過線程對象控制線程了,新線程會在后臺運(yùn)行,其所有權(quán)和控制權(quán)將會交給c++運(yùn)行庫。同時,C++運(yùn)行庫保證,當(dāng)線程退出時,其相關(guān)資源的能夠正確的回收。
注意:線程對象銷毀前,要么以jion()的方式等待線程結(jié)束,要么以detach()的方式將線程與線程對象分離。
二、原子性操作庫
多線程最主要的問題是共享數(shù)據(jù)帶來的問題(即線程安全)。如果共享數(shù)據(jù)都是只讀的,那么沒問題,因為只讀操作不會影響到數(shù)據(jù),更不會涉及對數(shù)據(jù)的修改,所以所有線程都會獲得同樣的數(shù)據(jù)。但是,當(dāng)一個或多個線程要修改共享數(shù)據(jù)時,就會產(chǎn)生很多潛在的麻煩。
2.1. atomic
雖然加鎖可以解決,但是加鎖有一個缺陷就是:只要一個線程在對sum++時,其他線程就會被阻塞,會影響程序運(yùn)行的效率,而且鎖如果控制不好,還容易造成死鎖。
因此C++11中引入了原子操作。所謂原子操作:即不可被中斷的一個或一系列操作,C++11引入的原子操作類型,使得線程間數(shù)據(jù)的同步變得非常高效。
注意:原子類型通常屬于"資源型"數(shù)據(jù),多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數(shù)中進(jìn)行構(gòu)造,不允許原子類型進(jìn)行拷貝構(gòu)造、移動構(gòu)造以及operator=等,為了防止意外,標(biāo)準(zhǔn)庫已經(jīng)將atmoic模板類中的拷貝構(gòu)造、移動構(gòu)造、賦值運(yùn)算符重載默認(rèn)刪除掉了。
#include<thread> #include<atomic> atomic<int>x = 0; void Add(int n) { for (int i = 0; i < n; i++) { x++; } } int main() thread t1(Add, 10000); thread t2(Add, 10000); t1.join(); t2.join(); cout << x << endl;//20000 return 0;
2.2. 鎖
在多線程環(huán)境下,如果想要保證某個變量的安全性,只要將其設(shè)置成對應(yīng)的原子類型即可,即高效又不容易出現(xiàn)死鎖問題。但是有些情況下,我們可能需要保證一段代碼的安全性,那么就只能通過鎖的方式來進(jìn)行控制。
1.lock、unlock:
線程函數(shù)調(diào)用lock()時,可能會發(fā)生以下三種情況:
- 如果該互斥量當(dāng)前沒有被鎖住,則調(diào)用線程將該互斥量鎖住,直到調(diào)用 unlock之前,該線程一直擁有該鎖。
- 如果當(dāng)前互斥量被其他線程鎖住,則當(dāng)前的調(diào)用線程被阻塞住。
- 如果當(dāng)前互斥量被當(dāng)前調(diào)用線程鎖住,則會產(chǎn)生死鎖(同一線程,不可以連續(xù)調(diào)用兩次鎖)。
線程函數(shù)調(diào)用try_lock()時,可能會發(fā)生以下三種情況:
- 如果當(dāng)前互斥量沒有被其他線程占有,則該線程鎖住互斥量,直到該線程調(diào)用 unlock 釋放互斥量。
- 如果當(dāng)前互斥量被其他線程鎖住,則當(dāng)前調(diào)用線程返回 false,而并不會被阻塞掉(非阻塞式)。
- 如果當(dāng)前互斥量被當(dāng)前調(diào)用線程鎖住,則會產(chǎn)生死鎖。
2.recursive_mutex:
允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權(quán),釋放互斥量時需要調(diào)用與該鎖層次深度相同次數(shù)的 unlock(),除此之外,recursive_mutex 的特性和mutex 大致相同。
3.timed_mutex:
比 mutex 多了兩個成員函數(shù),try_lock_for(),try_lock_until()
try_lock_for()
接受一個時間范圍,表示在這一段時間范圍之內(nèi)線程如果沒有獲得鎖則被阻塞?。ㄅc std::mutex的 try_lock() 不同,try_lock 如果被調(diào)用時沒有獲得鎖則直接返回 false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內(nèi)還是沒有獲得鎖),則返回 false。
try_lock_until()
接受一個時間點作為參數(shù),在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內(nèi)還是沒有獲得鎖),則返回 false。
int sum = 0; timed_mutex Lock; chrono::milliseconds timeout(100);//100毫秒 void func(int number) { for (int i = 0; i < number; i++) { //Lock.try_lock_for(timeout);//接收一個時間范圍 Lock.try_lock_until(chrono::steady_clock::now() + timeout);//接收一個時間點 sum++; Lock.unlock(); } }
4.recursive_timed_mutex
是recursive_mutex、timed_mutex兩種鎖的結(jié)合。
5.lock_guard(守衛(wèi)鎖):
加鎖和解鎖的過程,通過類的構(gòu)造和析構(gòu)來自動控制加鎖和釋放鎖(RAII思想)
template<class _Mutex> class lock_guard { public: // 構(gòu)造函數(shù)加鎖 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } //通過析構(gòu)函數(shù)釋放鎖 ~lock_guard() _NOEXCEPT _MyMutex.unlock(); //防拷貝和防賦值 lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex; };
int sum = 0; mutex Lock; void func(int number) { for (int i = 0; i < number; i++) { lock_guard<mutex> lg(Lock); sum++; } }
lock_guard類模板主要是通過RAII的方式,對其管理的互斥量進(jìn)行了封裝,在需要
加鎖的地方,只需要用上述介紹的任意互斥體實例化一個lock_guard,調(diào)用構(gòu)造函數(shù)成功上鎖,出作用域前,lock_guard對象要被銷毀,調(diào)用析構(gòu)函數(shù)自動解鎖,可以有效避免死鎖問題。
lock_guard的缺陷:太單一,用戶沒有辦法對該鎖進(jìn)行控制,因此C++11又提供了unique_lock。
6.unique_lock
與lock_gard類似,unique_lock類模板也是采用RAII的方式對鎖進(jìn)行了封裝,并且也是以獨(dú)占所有權(quán)的方式管理mutex對象的上鎖和解鎖操作,即其對象之間不能發(fā)生拷貝。在構(gòu)造(或移動(move)賦值)時,unique_lock 對象需要傳遞一個 Mutex 對象作為它的參數(shù),新創(chuàng)建的 unique_lock 對象負(fù)責(zé)傳入的 Mutex對象的上鎖和解鎖操作。使用以上類型互斥量實例化unique_lock的對象時,自動調(diào)用構(gòu)造函數(shù)上鎖,unique_lock對象銷毀時自動調(diào)用析構(gòu)函數(shù)解鎖,可以很方便的防止死鎖問題。 與lock_guard不同的是,unique_lock更加的靈活,提供了更多的成員函數(shù):
- 上鎖/解鎖操作:lock、try_lock、try_lock_for、try_lock_until和unlock
- 修改操作:移動賦值、交換(swap:與另一個unique_lock對象互換所管理的互斥量所有權(quán))、釋放(release:返回它所管理的互斥量對象的指針,并釋放所有權(quán))
- 獲取屬性:owns_lock(返回當(dāng)前對象是否上了鎖)、operator bool()(與owns_lock()的功能相同)、mutex(返回當(dāng)前unique_lock所管理的互斥量的指針)。
三、使用lambda表達(dá)式創(chuàng)建多個線程
#include<iostream> #include<thread> #include<atomic> #include<vector> using namespace std; int main() { atomic<int>x = 0; //m個線程對x加n次 int m, n; cin >> m >> n; vector<thread>vthreads; for (int i = 0; i < m; i++) { vthreads.push_back(thread([&x](int count) { for (int i = 0; i < count; i++) { x++; } }, n)); } for (auto& t : vthreads) cout << t.get_id() <<".join()" << endl; t.join(); cout << x << endl; return 0; }
四、條件變量
需要注意的是,傳入wait的鎖不能是lock_guard,這是因為lock_guard沒有解鎖接口
喚醒:
Notify one -> 喚醒一個
notify_all -> 喚醒一批
#include<iostream> #include<thread> #include<mutex> #include<condition_variable> using namespace std; int main() { int n = 20; mutex mtx1,mtx2; unique_lock<mutex> t; condition_variable cv1,cv2; //使用兩個線程打印0-n的數(shù),一個打印奇數(shù),一個打印偶數(shù) thread t1([&]() { for (int i = 0; i < n; i+=2) { cout << this_thread::get_id() << ":" << i << endl; cv2.notify_one();//打印偶數(shù)以后通知t2 unique_lock<mutex> lock(mtx1); cv1.wait(lock); } }); thread t2([&]() { for (int i = 1; i < n; i+=2) { unique_lock<mutex> lock(mtx2); cv2.wait(lock); cout << this_thread::get_id() << ":" << i << endl; cv1.notify_one();//t2打印奇數(shù)以后,通知t1 } }); t1.join(); t2.join(); return 0; }
到此這篇關(guān)于詳解C++11中的線程庫的文章就介紹到這了,更多相關(guān)C++11線程庫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解在C++中顯式默認(rèn)設(shè)置的函數(shù)和已刪除的函數(shù)的方法
這篇文章主要介紹了在C++中顯式默認(rèn)設(shè)置的函數(shù)和已刪除的函數(shù)的方法,文中講到了C++11標(biāo)準(zhǔn)中的新特性,需要的朋友可以參考下2016-01-01C++封裝遠(yuǎn)程注入類CreateRemoteThreadEx實例
這篇文章主要介紹了C++封裝遠(yuǎn)程注入類CreateRemoteThreadEx實例,詳細(xì)講述了注入DLL到指定的地址空間以及從指定的地址空間卸載DLL的方法,需要的朋友可以參考下2014-10-10C語言經(jīng)典例程100例(經(jīng)典c程序100例)
這篇文章主要介紹了C語言經(jīng)典例程100例,經(jīng)典c程序100例,學(xué)習(xí)c語言的朋友可以參考一下2018-03-03