C++11详解
目錄
前言
一、統一的列表初始化
1、{}初始化
2、std::itializer_list?
二、?右值引用和移動語義?
1、概念
2、左值引用和右值引用比較
3、右值引用使用場景和意義
3.1 移動構造和移動賦值
3.2 move函數
?3.3 右值引用傳參
4、完美轉發?
三、新的類功能
1、新增默認成員函數
?2、default和delete
四、可變模板參數
五、lambda表達式?
1、概念
2、函數對象與lambda表達式
六、包裝器
七、線程庫
1、簡單介紹
2、原子性操作庫
3、?lock_guard與unique_lock
3.1 mutex的種類
3.2 lock_guard
3.3?unique_lock?
八、舉例:兩個線程交替打印
總結
前言
C++11對比C++98帶來了數量可觀的變化,增加了很多新特性。相比較而言C++11能更好地用于系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率,公司實際項目開發中也用得比較多。所以對C++11的學習是很重要的。
一、統一的列表初始化
1、{}初始化
在C++98中,標準允許使用花括號{}對數組或者結構體元素進行統一的列表初始值設定。C++11擴大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。
int main() {int x1 = 1;//我們知道有不加等號這種寫法即可,自己寫的時候最好還是要加上int x2{ 2 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };// C++11中列表初始化也可以適用于new表達式中int* pa = new int[4]{ 0 };return 0; }同時也可以用來初始化對象:
class Date { public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;} private:int _year;int _month;int _day; }; int main() {Date d1(2022, 1, 1); // old style// C++11支持的列表初始化,這里會調用構造函數初始化Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0; }2、std::itializer_list?
std::itializer_list是一種什么類型呢?
int main() {// the type of il is an initializer_listauto il = { 10, 20, 30 };cout << typeid(il).name() << endl;return 0; }可以看出編譯器會將大括號轉化為std::itializer_list類型。
?Initializer_list是c++11新增加的一個類型,和數組類似,它主要用來初始化。std::initializer_list一般是作為構造函數的參數,C++11對STL中的不少容器就增加std::initializer_list作為參數的構造函數。
?這樣初始化容器對象就更方便了。也可以作為operator=的參數,這樣就可以用大括號賦值。
int main() { vector<int> v = { 1,2,3,4 }; list<int> lt = { 1,2 }; // 這里{"sort", "排序"}會先初始化構造一個pair對象 map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; // 使用大括號對容器賦值 v = {10, 20, 30}; return 0;二、?右值引用和移動語義?
1、概念
傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現在開始我們
之前學習的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
什么是左值?什么是右值?
左值可以取地址,可以出現在等號左邊,可以改變值的大小(const類型除外)。
右值不可以被取地址,不可以出現在等號左邊,只能出現在等號右邊,不可以改變值的大小。右值一般有兩種,一種是純右值,一種是將亡值(函數返回的臨時變量)。
2、左值引用和右值引用比較
左值引用總結:
- 左值引用一般情況下只能引用左值,不能引用右值。
- 但是const左值引用既可以引用左值也可以引用右值。
?右值引用總結:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。move是一個函數,可以將左值轉化為右值。
需要注意的是,右值雖然不能被取地址,但右值被取別名后會存儲到特定的位置,這個特定位置的地址是可以被取地址的。也就是說字面量10為右值不能被取地址,但是右值引用取別名為r1后,r1的地址是可以取到的,并且r1的值是可以被修改的,如果不想rr1被修改,可以用const int&& r1 去引用。當然右值引用的實際用處并不在此,只需要了解即可。
3、右值引用使用場景和意義
3.1 移動構造和移動賦值
前面我們可以看到左值引用加const后既可以引用左值和又可以引用右值,那為什么C++11還要提出右值引用呢?原因是右值引用可以補足左值引用的短板。
我們先來看一下左值引用的優勢:在左值引用做參數或者做返回值時能夠很好的減少拷貝次數,從而提高效率。
void func1(bit::string s) {} void func2(const bit::string& s) {} int main() {bit::string s1("hello world");// func1和func2的調用我們可以看到左值引用做參數減少了拷貝,提高效率的使用場景和價值func1(s1);func2(s1);// string& operator+=(char ch) 傳左值引用沒有拷貝提高了效率s1 += '!';return 0; }但是當函數返回對象是一個局部變量,出了函數作用域就不存在了,就不能使用左值引用返回,只能傳值返回。例如:bit::string to_string(int value)函數中可以看到,這里只能使用傳值返回,傳值返回會導致至少1次拷貝構造(如果是一些舊一點的編譯器可能是兩次拷貝構造,先把這個值拷貝給一個臨時變量,然后臨時變量在拷貝給接收值)。
在值拷貝過程中發生了深拷貝,效率是很低的,為了解決這一問題,c++增加了移動構造和移動賦值。
移動構造:
// 移動構造//初始化一下數據,和s交換string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移動構造" << endl;this->swap(s);}int main(){bit::string ret2 = bit::to_string(-1234);return 0;}?在發生拷貝時,編譯器會優先把bit::string to_string(int value)函數的返回值當成右值處理,如果類中有移動構造則調用移動構造,沒有移動構造再調用拷貝構造。
移動賦值:
只有在定義ret1時接收返回值編譯器才能進行優化成一次構造,像下面這樣寫編譯器需要將bit::string to_string(1234)的值構造形成臨時變量,再將臨時變量賦值給ret1。上面提到過,編譯器會優先返回值當成右值去處理,故移動構造出臨時變量,再移動賦值給ret1。
// 移動賦值 string& operator=(string&& s) {cout << "string& operator=(string&& s) -- 移動語義" << endl;this->swap(s);return *this; } int main() {bit::string ret1;ret1 = bit::to_string(1234);return 0; }// 運行結果: // string(string&& s) -- 移動構造 // string& operator=(string&& s) -- 移動賦值移動構造和移動賦值的效率是很高的,它的本質是把右值的資源竊取過來占為己有,就不用再花力氣拷貝了。反正右值一般是會快速消失的,即便資源被竊取了也不會有影響。
3.2 move函數
按照語法,右值引用只能引用右值,但右值引用一定不能引用左值嗎?因為:有些場景下,可能真的需要用右值去引用左值實現移動語義。當需要用右值引用引用一個左值時,可以通過move函數將左值轉化為右值。C++11中,std::move()函數位于 頭文件中,該函數名字具有迷惑性,它并不搬移任何東西,唯一的功能就是將一個左值強制轉化為右值引用,然后實現移動語義。?
但要注意,被強制轉化為右值的左值資源可能會被掠奪,掠奪之后不能在使用這些資源,否則會出錯。?
int main() {bit::string s1("hello world");// 這里s1是左值,調用的是拷貝構造bit::string s2(s1);// 這里我們把s1 move處理以后, 會被當成右值,調用移動構造// 但是這里要注意,一般是不要這樣用的,因為我們會發現s1的// 資源被轉移給了s3,s1被置空了。bit::string s3(std::move(s1));return 0; }?3.3 右值引用傳參
C++11后,STL容器插入接口函數也增加了右值引用做參數的版本。
int main() {list<bit::string> lt;bit::string s1("1111");// 這里調用的是拷貝構造lt.push_back(s1);// 下面調用都是移動構造lt.push_back("2222");lt.push_back(std::move(s1));return 0; }?如果是左值引用插入在申請節點空間的時候會采用拷貝構造,如果是右值引用插入則會使用移動構造。
4、完美轉發?
模板中的&& 萬能引用:?
- 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
- 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力?。
我們通過代碼測試一下,看看萬能模板是不是如我們想象的那樣,注釋的內容是理想的打印內容:
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T> void PerfectForward(T&& t) {Fun(t); } int main() {PerfectForward(10); ?????// 右值int a;PerfectForward(a); ??????// 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); ???// const 左值PerfectForward(std::move(b)); // const 右值return 0; }實際情況并不像我們想象的那樣,打印出來的全部都是左值引用。
?這是因為右值引用的對象,再次傳遞時會退化成左值引用,其實這很好理解,上文我們提到過,右值一旦被引用就會被存在一個特定的地方并且可以取到地址,所以再次使用時編譯器就會把它當成左值了。想要保持它的右值屬性,就要使用完美轉發。
?std::forward 完美轉發在傳參的過程中保留對象原生類型屬性:
只要在傳參是時候加上完美轉發,就可以保留右值屬性。
template<typename T> void PerfectForward(T&& t) {Fun(std::forward<T>(t)); }在實際中的應用:
void Insert(Node* pos, T&& x) {Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 關鍵位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode; }三、新的類功能
1、新增默認成員函數
原來C++類中,有6個默認成員函數:
- 構造函數
- 析構函數
- 拷貝構造函數
- 拷貝賦值重載
- 取地址重載
- const 取地址重載
當然,我們平常很少用到后兩個。在C++11中,又新增了兩個默認構造函數,分別是移動構造和移動賦值。
- 如果你沒有自己實現移動構造函數,且沒有實現析構函數 、拷貝構造、拷貝賦值重載中的任意一個。那么編譯器會自動生成一個默認移動構造。默認生成的移動構造函數,對于內置類型成員會執行逐成員按字節拷貝,自定義類型成員,則需要看這個成員是否實現移動構造,如果實現了就調用移動構造,沒有實現就調用拷貝構造。
- 如果你沒有自己實現移動賦值重載函數,且沒有實現析構函數 、拷貝構造、拷貝賦值重載中的任意一個,那么編譯器會自動生成一個默認移動賦值。。(默認移動賦值跟上面移動構造完全類似)
?2、default和delete
強制生成默認函數的關鍵字default:
C++11可以讓你更好的控制要使用的默認函數。假設你要使用某個默認的函數,但是因為一些原因這個函數沒有默認生成。比如:我們提供了拷貝構造,就不會生成移動構造了,那么我們可以使用default關鍵字顯示指定移動構造生成。
Person(Person&& p) = default;禁止生成默認函數的關鍵字delete:?
如果能想要限制某些默認函數的生成,在C++98中,是該函數設置成private,并且只聲明補丁
已,這樣只要其他人想要調用就會報錯。在C++11中更簡單,只需在該函數聲明加上=delete即
可,該語法指示編譯器不生成對應函數的默認版本,稱=delete修飾的函數為刪除函數。
四、可變模板參數
C++11的新特性可變參數模板能夠讓您創建可以接受可變參數的函數模板和類模板,相比C++98,類模版和函數模版中只能含固定數量的模版參數,可變模版參數無疑是一個巨大的改進。
// Args是一個模板參數包,args是一個函數形參參數包 // 聲明一個參數包Args...args,這個參數包中可以包含0到任意個模板參數。 template <class ...Args> void ShowList(Args... args) {}上面的參數args前面有省略號,所以它就是一個可變模版參數,我們把帶省略號的參數稱為“參數包”,它里面包含了0到N(N>=0)個模版參數。我們無法直接獲取參數包args中的每個參數的,只能通過展開參數包的方式來獲取參數包中的每個參數,這是使用可變模版參數的一個主要特點,也是最大的難點,即如何展開可變模版參數。由于語法不支持使用args[i]這樣方式獲取可變參數,所以我們的用一些特殊的方法來獲取參數包的值。?
// 遞歸終止函數 template <class T> void ShowList(const T& t) {cout << t << endl; } // 展開函數 template <class T, class ...Args> void ShowList(T value, Args... args) {cout << value << " ";ShowList(args...); } int main() {ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0; }在上面的展開函數中,每次 都會取參數包中第一個值出來,剩下的作為一個新的參數包遞歸下去。要注意的是想終止遞歸必須要單獨寫一個遞歸終止函數,不能在展開函數中終止。因為模板推演是一個編譯是邏輯,而在展開函數中終止是運行時邏輯,不能達到目的。
STL容器中的empalce相關接口函數:
在C++11中很多容器添加了emplace系列的接口,支持模板的可變參數,并且萬能引用。
emplace和insert都是用來插入的函數,那么相對insert和emplace系列接口的優勢到底在哪里呢?
其實一般來說,emplace和insert并沒有太大區別,對于某些情況下來說來說,效率會得到一定的提升。emplace_back支持可變參數,拿到構建pair對象的參數后自己去創建對象。
int main() {// 我們會發現其實差別也不到,emplace_back是直接構造了// ,push_back是先構造,再移動構造,其實也還好。std::list< std::pair<int, bit::string> > mylist;mylist.emplace_back(10, "sort");//會直接去調構造函數mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort" });return 0; }五、lambda表達式?
1、概念
在C++98中,如果想要對一個數據集合中的元素進行排序,可以使用sort函數。如果待排序元素為自定義類型,需要用戶使用仿函數定義排序時的比較規則。隨著C++語法的發展,人們開始覺得上面的寫法太復雜了,每次為了實現一個algorithm算法,都要重新去寫一個類,如果每次比較的邏輯不一樣,還要去實現多個類,特別是相同類的命名,這些都給編程者帶來了極大的不便。因此,在C++11語法中出現了Lambda表達式。
lambda表達式語法:
lambda表達式書寫格式:[capture-list] (parameters) mutable -> return-type { statement}
- [capture-list]:捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[ ]判斷接下來的函數是否為lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda函數使用。
- ?(parameters):參數列表。與普通函數的參數列表一致,如果不需要參數傳遞,則可以
連同()一起省略。 - mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。
- ->returntype:返回值類型。用追蹤返回類型形式聲明函數的返回值類型,沒有返回值時此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。
- {statement}:函數體。在該函數體內,除了可以使用其參數外,還可以使用所有捕獲
到的變量。
注意:
在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空。因此C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情。
捕獲列表說明:
- 捕捉列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是傳引用。
- [var]:表示值傳遞方式捕捉變量var。
- [=]:表示值傳遞方式捕獲所有父作用域中的變量(包括成員函數中的this)。
- [&var]:表示引用傳遞捕捉變量var。
- [&]:表示引用傳遞捕捉所有父作用域中的變量(包括成員函數中的this)。
- [this]:表示值傳遞方式捕捉當前的this指針。
2、函數對象與lambda表達式
函數對象,又稱為仿函數,即可以想函數一樣使用的對象,就是在類中重載了operator()運算符的
類對象。
lambda表達式可以很好的代替仿函數使用,如果使用仿函數,閱讀代碼的人想要知道其中的邏輯還需要去對應的地方去找,而lambda表達式可以很好的避免這個問題,增強了代碼的可讀性:
int main() { vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠蘿", 1.5, 4 } }; sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){ return g1._price < g2._price; }); }從使用方式上來看,函數對象與lambda表達式完全一樣。實際在底層編譯器對于lambda表達式的處理方式,完全就是按照函數對象的方式處理的,即:如果定義了一個lambda表達式,編譯器會自動生成一個類,在該類中重載了operator()。?
六、包裝器
function包裝器 也叫作適配器。C++中的function本質是一個類模板,也是一個包裝器。那么我們來看看,我們為什么需要function呢?
ret = func(x);
上面的func可能是什么呢?可能是函數名,可能是函數指針,可能是仿函數,也可能是lambda表達式。這些都是可調用對象,可以做函數參數。
template<class F, class T> T useF(F f, T x) {return f(x); } int main() {// 函數名cout << useF(f, 11.11) << endl;// 函數對象cout << useF(Functor(), 11.11) << endl;// lamber表達式cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;return 0; }通過上面的程序驗證,我們會發現useF函數模板實例化了三份,很浪費資源,包裝器可以很好的解決這個問題。
//std::function在頭文件<functional> template <class Ret, class... Args> class function<Ret(Args...)>;模板參數說明:
- Ret: 被調用函數的返回類型
- Args…:被調用函數的形參
包裝器的作用:統一可調用對象的類型,指明了參數和返回值類型
不包裝前可能存在很多問題:
- 函數指針太復雜,不方便理解。
- 仿函數類型是一個類名,沒有指明參數和返回值,需要去operator()才能看出來。
- lambda表達式在語法層看不到類型。
七、線程庫
1、簡單介紹
在C++11之前,涉及到多線程問題,都是和平臺相關的,比如windows和linux下各有自己的接
口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進行支持了,使得C++在
并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。要使用標準庫中的
線程,必須包含< thread >頭文件。
注意:
- 線程是操作系統中的概念,線程對象可以關聯一個線程,用來控制線程和獲取線程的狀態。
- 當創建一個線程后,如果沒有提供任何線程函數,該對象實際沒有對應任何線程。
線程對象構造函數:
- ?第一個構造函數創建了空線程對象,沒有關聯任何線程函數。
- 第二個構造函數:第一個參數為線程函數,之后傳的是線程函數的參數。
- 線程對象不能拷貝,但可以轉移(常常配合空線程對象使用)。
創建一個線程:
#include <thread> int main() {std::thread t1;cout << t1.get_id() << endl;return 0; }當創建一個線程對象后,并且給線程關聯線程函數,該線程就被啟動,與主線程一起運行。線程函數一般情況下可按照以下三種方式提供:?
int main() {// 線程函數為函數指針thread t1(ThreadFunc, 10);// 線程函數為lambda表達式thread t2([]{cout << "Thread2" << endl; });// 線程函數為函數對象TF tf;thread t3(tf);t1.join();t2.join();t3.join();cout << "Main thread!" << endl;return 0; }jionable() 函數:
可以通過jionable()函數判斷線程是否是有效的,如果是以下任意情況,則線程無效:
- 采用無參構造函數構造的線程對象
- 線程對象的狀態已經轉移給其他線程對象
- 線程已經調用jion或者detach結束
其它接口和Linux下類似,查閱相關文檔學習即可。
2、原子性操作庫
多線程最主要的問題是共享數據帶來的問題(即線程安全)。如果共享數據都是只讀的,那么沒問題,因為只讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據。但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩。
傳統的解決辦法是加鎖保護,雖然加鎖可以解決,但是加鎖有一個缺陷,就是在加鎖過程中其它線程會被阻塞,影響效率。如果加鎖僅僅是為了完成一些簡單操作,就會顯得非常不劃算。而且鎖如果控制不好,還容易造成死鎖。所以在c++11中引入了原子操作。
所謂原子操作:即不可被中斷的一個或一系列操作,引入的原子操作類型使得線程間數據的同步變得非常高效。
需要使用以上原子操作變量時,必須添加頭文件。在C++11中,程序員不需要對原子類型變量進行加鎖解鎖操作,線程能夠對原子類型變量互斥的訪問。?
更為普遍的,程序員可以使用atomic類模板,定義出需要的任意原子類型。
atmoic<T> t; ??// 聲明一個類型為T的原子類型變量t注意:原子類型通常屬于"資源型"數據,多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數中進行構造,不允許原子類型進行拷貝構造、移動構造以及operator=等,為了防止意外,標準庫已經將atmoic模板類中的拷貝構造、移動構造、賦值運算符重載默認刪除掉了。?
3、?lock_guard與unique_lock
在多線程環境下,如果想要保證某個變量的安全性,只要將其設置成對應的原子類型即可,即高
效又不容易出現死鎖問題。但是有些情況下,我們可能需要保證一段代碼的安全性,那么就只能
通過鎖的方式來進行控制。
3.1 mutex的種類
在C++11中,Mutex總共包了四個互斥量的種類:
(1)mutex
| 函數名 | 函數功能 |
| lock()? | 上鎖:鎖住互斥量 |
| unlock()? | 解鎖:釋放對互斥量的所有權 |
| try_lock() | 嘗試鎖住互斥量,如果互斥量被其他線程占有,則當前線程也不會被阻塞 |
線程函數調用try_lock()時,可能會發生以下三種情況:
- 如果當前互斥量沒有被其他線程占有,則該線程鎖住互斥量,直到該線程調用 unlock釋放互斥量。
- 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而并不會被阻塞掉。
- 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
(2)recursive_mutex
如果在遞歸函數中使用普通鎖,會發生重復加鎖導致死鎖。這時候就要用到recursive_mutex,其允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
(3)timed_mutex
比mutex 多了兩個成員函數,try_lock_for(),try_lock_until() 。
- try_lock_for()
接受一個時間范圍,表示在這一段時間范圍之內線程如果沒有獲得鎖則被阻塞住(與std::mutex 的 try_lock() 不同,try_lock 如果被調用時沒有獲得鎖則直接返回false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
- try_lock_until()
接受一個時間點作為參數,在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
(4)recursive_timed_mutex
3.2 lock_guard
在使用鎖時很容易發生死鎖問題,比如在解鎖之前函數進行了return或報異常。因此:C++11采用RAII的方式對鎖進行了封裝,即lock_guard和unique_lock。
lock_gurad 是 C++11 中定義的模板類。定義如下:
template<class Lock>class lock_guard{public:lock_guard(Lock& lock):_lock(lock){_lock.lock();cout << "加鎖" << endl;}~lock_guard(){_lock.unlock();cout << "解鎖" << endl;}lock_guard(const lock_guard<Lock>& lock) = delete;lock_guard& operator=(const lock_guard& lock) = delete;private:Lock& _lock;};在構造lock_guard對象的時候會自動上鎖,當出作用域lock_guard對象析構的時候又會自動解鎖,可以有效避免死鎖問題。
lock_guard缺陷:太單一,用戶沒有辦法對該鎖進行控制,因此C++11又提供了unique_lock。
3.3?unique_lock?
與lock_gard類似,unique_lock類模板也是采用RAII的方式對鎖進行了封裝,并且也是以獨占所
有權的方式管理mutex對象的上鎖和解鎖操作,即其對象之間不能發生拷貝。與lock_guard不同的是,unique_lock更加的靈活,提供了更多的成員函數:
上鎖/解鎖操作:lock、try_lock、try_lock_for、try_lock_until和unlock
八、舉例:兩個線程交替打印
我們最先想到的是下面這種方法:
int main() {int n = 100;mutex mtx;// 奇數 假設 thread2遲遲沒創建好或者沒有排到cpu時間片,就會導致t1連續打印奇數,不符合題意要求thread t1([&](){int i = 1;for (; i < n; i += 2){unique_lock<mutex> lock(mtx);cout << i << endl;}});// 偶數thread t2([&](){int j = 2;for (; j < n; j += 2){unique_lock<mutex> lock(mtx);cout << j << endl;}});t1.join();t2.join();return 0; }這種方法在大部分情況下確實可以滿足要求,但在某些情況下會出現問題:
- 我們無法保證thread t1和?thread t2哪個線程先創建,即便我們在代碼中先創建thread t1,但在極少數情況下依舊會出現thread t2先創建的情況。
- 如果thread t2因為某些原因遲遲沒有創建好,或者沒有排到cpu的時間片,會出現thread t1連續打印的情況。
為了解決上面的問題,要使用到條件變量。在c++中,條件變量的wait接口比較特殊:
可以看到第二個接口中增加了pred參數,predicate這個單詞的意思是以什么為根據,?pred需要傳一個可調用對象,以這個可調用對象的返回值為根據做出判斷。
?如果pred的返回值是false,則線程會一直阻塞,即便被喚醒了也會再次陷入等待。如果是true則會繼續往下運行。
代碼如下:
int main() {int n = 100;mutex mtx;condition_variable cv;bool flag = true;//打印奇數thread t1([&](){for (int i = 1; i <=n; ){//這里要用unique_lock,因為在線程等待的時候需要對鎖進行釋放,需要有釋放鎖的接unique_lock<mutex> lock(mtx);cv.wait(lock, [&flag] {return flag; });cout << i << endl;i += 2;flag = false;cv.notify_one();}});//打印偶數thread t2([&](){for (int j = 2; j <= n; ){//這里要用unique_lock,因為在線程等待的時候需要對鎖進行釋放,需要有釋放鎖的接unique_lock<mutex> lock(mtx);cv.wait(lock, [&flag] {return !flag; });cout << j << endl;j += 2;flag = true;cv.notify_one();}});t1.join();t2.join(); }這樣可以很少的解決特殊情況下可能出現的問題。比如線程1啟動后線程2遲遲沒有啟動起來,這時候線程1是無法做到多次打印的,因為打印一次后flag就會變為true,線程1wait的時候就會被阻塞住,必須等線程2啟動后調整flag的值。再或者線程2剛剛釋放鎖還沒來得及進入等待隊列就被切走了,同理線程1在線程2被切走后的這段時間內打印一次就會被阻塞,必須等線程2切回來后改變falg的值才能被喚醒。
總結
本文主要簡單介紹了C++11新增的語法特性,希望可以給大家帶來幫助。江湖路遠,來日方長,我們下次見。
總結
- 上一篇: Qt 模型视图编程之表头设置
- 下一篇: 【转】提高MATLAB运行效率