深入解析C++11?lambda表達式/包裝器/線程庫
零、前言
本章是講解學(xué)習(xí)C++11語法新特性的第三篇文章,主要學(xué)習(xí)lambda表達式,包裝器,線程庫
一、lambda表達式
1、lambda的引入
在C++98中,如果想要對一個數(shù)據(jù)集合中的元素進行排序,可以使用std::sort方法
示例:
#include <algorithm> #include <functional> int main() { int array[] = { 4,1,8,5,3,7,0,9,2,6 }; // 默認按照小于比較,排出來結(jié)果是升序 std::sort(array, array + sizeof(array) / sizeof(array[0])); for (int i = 0; i < 10; i++) { cout << array[i] << " "; }cout << endl; // 如果需要降序,需要改變元素的比較規(guī)則 std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>()); for (int i = 0; i < 10; i++) { cout << array[i] << " "; }cout << endl; return 0; }
效果:
注:如果待排序元素為自定義類型,需要用戶定義排序時的比較規(guī)則
示例:
struct Goods { string _name; double _price; }; struct Compare { bool operator()(const Goods& gl, const Goods& gr) { return gl._price <= gr._price; } }; int main() { Goods gds[] = { { "蘋果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠蘿", 1.5} }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare()); for (int i = 0; i < 4; i++) cout << gds[i]._name << ":"<<gds[i]._price<<" "; cout << endl; return 0; }
效果:
概念及引入:
隨著C++語法的發(fā)展,人們開始覺得上面的寫法太復(fù)雜了,每次為了實現(xiàn)一個algorithm算法, 都要重新去寫一個類,如果每次比較的邏輯不一樣,還要去實現(xiàn)多個類,特別是相同類的命名,這些都給編程者帶來了極大的不便。因此,在C11語法中出現(xiàn)了Lambda表達式
示例:
int main() { Goods gds[] = { { "蘋果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠蘿", 1.5} }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r) ->bool { return l._price < r._price; }); return 0; }
注:可以看出lamb表達式實際是一個匿名函數(shù)
2、lambda表達式語法
lambda表達式書寫格式:
[capture-list] (parameters) mutable -> return-type { statement }
lambda表達式各部分說明:
[capture-list] :
捕捉列表,該列表總是出現(xiàn)在lambda函數(shù)的開始位置,編譯器根據(jù)[]來判斷接下來的代碼是否為lambda函數(shù),捕捉列表能夠捕捉上下文中的變量供lambda函數(shù)使用
(parameters):
參數(shù)列表,與普通函數(shù)的參數(shù)列表一致,如果不需要參數(shù)傳遞,則可以連同()一起省略
mutable:
默認情況下,lambda函數(shù)總是一個const函數(shù), mutable的作用就是讓傳值捕捉的對象可以修改,但是你修改的是傳值拷貝的對象,不影響外面對象,使用該修飾符時,參數(shù)列表不可省略(即使參數(shù)為空)
注:實際中mutable意義不大,除非你就是想傳值捕捉過來,lambda中修改,不影響外面的值
->returntype:
返回值類型,用追蹤返回類型形式聲明函數(shù)的返回值類型,沒有返回值時此部分可省略;返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導(dǎo)
{statement}:
函數(shù)體,在該函數(shù)體內(nèi),除了可以使用其參數(shù)外,還可以使用所有捕獲到的變量
注:在lambda函數(shù)定義中,參數(shù)列表和返回值類型都是可選部分,而捕捉列表和函數(shù)體可以為空,即C++11中最簡單的lambda函數(shù)為:[]{}; 該lambda函數(shù)不能做任何事情
示例:
int main() { // 最簡單的lambda表達式, 該lambda表達式?jīng)]有任何意義 [] {}; // 省略參數(shù)列表和返回值類型,返回值類型由編譯器推導(dǎo)為int int a = 3, b = 4; [=]{ return a + 3; }; // 省略了返回值類型,無返回值類型 auto fun1 = [&](int c) { b = a + c; }; fun1(10); cout << a << " " << b << endl; // 各部分都很完善的lambda函數(shù) auto fun2 = [=, &b](int c)->int {return b += a + c; }; cout << fun2(10) << endl; // 復(fù)制捕捉x int x = 10; auto add_x = [x](int a)mutable { x *= 2; return a + x; };//傳值捕捉修改需要mutable修飾 auto add_x1 = [&x](int a){ x *= 2; return a + x; };//引用捕捉不用 cout << add_x(10) << endl; cout << x << endl; cout << add_x1(10) << endl; cout << x << endl; return 0; }
效果:
注:lambda表達式實際上可以理解為無名函數(shù),該函數(shù)無法直接調(diào)用,如果想要直接調(diào)用,可借助auto將其賦值給一個變量
3、捕獲列表說明
概念:
捕捉列表描述了上下文中那些數(shù)據(jù)可以被lambda使用,以及使用的方式傳值還是傳引用
使用方式:
[var]:表示值傳遞方式捕捉變量var [=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this) [&var]:表示引用傳遞捕捉變量var [&]:表示引用傳遞捕捉所有父作用域中的變量(包括this) [this]:表示值傳遞方式捕捉當(dāng)前的this指針
注意:
父作用域指包含lambda函數(shù)的語句塊
語法上捕捉列表可由多個捕捉項組成,并以逗號分割:比如:[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量 [&,a, this]:值傳遞方式捕捉變量a和this,引用方式捕捉其他變量
捕捉列表不允許變量重復(fù)傳遞,否則就會導(dǎo)致編譯錯誤:比如:[=, a]:=已經(jīng)以值傳遞方式捕捉了所有變量,捕捉a重復(fù)
在塊作用域以外的lambda函數(shù)捕捉列表必須為空;在塊作用域中的lambda函數(shù)僅能捕捉父作用域中局部變量
lambda表達式之間不能相互賦值,即使看起來類型相同
示例:
void (*PF)(); int main() { auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2; // 編譯失敗--->提示找不到operator=() // 允許使用一個lambda表達式拷貝構(gòu)造一個新的副本 auto f3(f2); f3(); // 可以將沒有捕獲任何變量的lambda表達式賦值給相同類型的函數(shù)指針 PF = f2; PF(); return 0; }
解釋:
Lambda是實現(xiàn)了函數(shù)調(diào)用運算符的匿名類(anonymous class)。對于每一個Lambda,編譯器創(chuàng)建匿名類,并定義相應(yīng)的數(shù)據(jù)成員存儲Lambda捕獲的變量。沒有捕獲變量的Lambda不包含任何含成員變量。一個沒有任何成員變量(包括沒有虛函數(shù)表指針)的類型,在空指針上調(diào)用成員函數(shù)也不會有任何的問題,因為它的成員函數(shù)不會通過this指針訪問內(nèi)存。當(dāng)Lambda向函數(shù)指針的轉(zhuǎn)換時,編譯器為Lambda的匿名類實現(xiàn)函數(shù)指針類型轉(zhuǎn)換運算符
4、函數(shù)對象與lambda表達式
函數(shù)對象,又稱為仿函數(shù),即可以想函數(shù)一樣使用的對象,就是在類中重載了operator()運算符的類對象
示例:
class Rate { public: Rate(double rate): _rate(rate) {} double operator()(double money, int year) { return money * _rate * year; } private: double _rate; }; int main() { // 函數(shù)對象 double rate = 0.49; Rate r1(rate); r1(10000, 2); // lamber auto r2 = [=](double monty, int year)->double{return monty*rate*year; }; r2(10000, 2); return 0; }
說明:
從使用方式上來看,函數(shù)對象與lambda表達式完全一樣:函數(shù)對象將rate作為其成員變量,在定義對象時給出初始值即可,lambda表達式通過捕獲列表可以直接將該變量捕獲到
示圖:
注:實際在底層編譯器對于lambda表達式的處理方式,完全就是按照函數(shù)對象的方式處理的
二、包裝器
1、function包裝器
概念:
function包裝器也叫作適配器,C++中的function本質(zhì)是一個類模板,也是一個包裝器
由于C++的歷史遺留問題,導(dǎo)致如果想實現(xiàn)一個函數(shù)功能,可以采用函數(shù)名、函數(shù)指針、仿函數(shù)、有名稱的lambda表達式,所有這些都是可調(diào)用的類型
存在的問題:
函數(shù)指針類型太復(fù)雜,不方便使用和理解仿函數(shù)類型是一個類名,沒有指定調(diào)用參數(shù)和返回值,得去看operator()的實現(xiàn)才能看出來lambda表達式在語法層,看不到類型,只能在底層看到其類型,基本都是lambda_uuid
示例:
template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函數(shù)名 cout << useF(f, 11.11) << endl; // 函數(shù)對象 cout << useF(Functor(), 11.11) << endl; // lamber表達式 cout << useF([](double d)->double { return d / 4; }, 11.11) << endl; return 0; }
效果:
注:對于函數(shù)名稱,仿函數(shù)對象,lambda表達式對象這些都是可調(diào)用的類型,我們發(fā)現(xiàn)發(fā)現(xiàn)useF函數(shù)模板實例化了三份,所以如此豐富的類型,可能會導(dǎo)致模板的效率低下,包裝器可以很好的解決該問題
包裝器原型:
// 類模板原型如下 template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>;
模板參數(shù)說明:
Ret: 被調(diào)用函數(shù)的返回類型
Args…:被調(diào)用函數(shù)的形參
注:std::function在頭文件< functional >
示例:
#include <functional> int f(int a, int b) { return a + b; } struct Functor { int operator() (int a, int b) { return a + b; } }; class Plus { public: static int plusi(int a, int b) { return a + b; } double plusd(double a, double b) { return a + b; } }; int main() { // 函數(shù)名(函數(shù)指針) std::function<int(int, int)> func1 = f; cout << func1(1, 2) << endl; // 函數(shù)對象 std::function<int(int, int)> func2 = Functor(); cout << func2(1, 2) << endl; // lamber表達式 std::function<int(int, int)> func3 = [](const int a, const int b){ return a + b; }; cout << func3(1, 2) << endl; // 類的成員函數(shù) std::function<int(int, int)> func4 = Plus::plusi; cout << func4(1, 2) << endl; std::function<double(Plus, double, double)> func5 = &Plus::plusd;//對于普通成員的包裝一定要加上&,需要通過指針進行調(diào)用成員函數(shù) cout << func5(Plus(), 1.1, 2.2) << endl;//傳入類對象,通過對象進行調(diào)用 return 0; }
效果:
包裝器解決模板實例化多份的問題:
#include <functional> template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { //將多個可調(diào)用類型進行封裝成相同類型,便于統(tǒng)一調(diào)用 // 函數(shù)名 std::function<double(double)> func1 = f; cout << useF(func1, 11.11) << endl; // 函數(shù)對象 std::function<double(double)> func2 = Functor(); cout << useF(func2, 11.11) << endl; // lamber表達式 std::function<double(double)> func3 = [](double d)->double { return d /4; }; cout << useF(func3, 11.11) << endl; return 0; }
效果:
2、bind 概念:
std::bind函數(shù)定義在頭文件中,是一個函數(shù)模板,它就像一個函數(shù)包裝器(適配器),接受一個可調(diào)用對象(callable object),生成一個新的可調(diào)用對象來“適應(yīng)”原對象的參數(shù)列表一般而言,我們用它可以把一個原本接收N個參數(shù)的函數(shù)fn,通過綁定一些參數(shù),返回一個接收M個(M可以大于N,但這么做沒什么意義)參數(shù)的新函數(shù);同時,使用std::bind函數(shù)還可以實現(xiàn)參數(shù)順序調(diào)整等操作
示例:
#include <functional> int Plus(int a, int b) { return a + b; } class Sub { public: int sub(int a, int b) { return a - b; } static int sub1(int a, int b) { return a - b; } }; int main() { //普通函數(shù)的綁定 //表示綁定函數(shù)plus 參數(shù)分別由調(diào)用 func1 的第一,二個參數(shù)指定(placeholders用來表示參數(shù)位占位) std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2); //auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);直接使用auto識別類型 //表示綁定函數(shù) plus 的第一,二參數(shù)為: 1, 2 auto func2 = std::bind(Plus, 1, 2); cout << func1(1, 2) << endl; cout << func2(2,3) << endl;//func2();也可以不用傳參數(shù)-因為參數(shù)已經(jīng)綁定好了,傳入的參數(shù)沒有實際的作用 //類函數(shù)的綁定 //類的成員函數(shù)必須通過類的對象或者指針調(diào)用,因此在bind時,bind的第一個參數(shù)的位置來指定一個類的實列、指針或引用。 Sub s; // 綁定成員函數(shù) std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2); // 參數(shù)調(diào)換順序 std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,placeholders::_2, placeholders::_1); std::function<int(int, int)> func5 = std::bind(Sub::sub1,placeholders::_2, placeholders::_1);//靜態(tài)成員函數(shù)的綁定-不需要類的示例指針或引用 cout << func3(1, 2) << endl; cout << func4(1, 2) << endl; cout << func5(1, 2) << endl; return 0; }
效果:
總結(jié):
bind是對包裝的可調(diào)用類型的進一步封裝,可以根據(jù)自己的需要進行調(diào)整參數(shù)的數(shù)據(jù)及位置,綁定類對象能有優(yōu)化成員函數(shù)的包裝使用,更加符合使用習(xí)慣
三、線程庫
1、線程的概念及使用
thread類的簡單介紹:
在C++11之前,涉及到多線程問題,都是和平臺相關(guān)的,比如windows和linux下各有自己的接口,這使得代碼的可移植性比較差C++11中最重要的特性就是對線程進行支持了,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念
注:要使用標準庫中的線程,必須包含< thread >頭文件
線程常用接口:
函數(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) |
注意:
線程是操作系統(tǒng)中的一個概念,是進程中的一個執(zhí)行分支,線程對象可以關(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; return 0; }
注:get_id()的返回值類型為id類型,id類型實際為std::thread命名空間下封裝的一個類,該類中包含了一個結(jié)構(gòu)體
對應(yīng)結(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ù),該線程就被啟動,與主線程一起運行
線程函數(shù)一般情況下可按照以下三種方式提供:
- 函數(shù)指針
- lambda表達式
- 函數(shù)對象
示例:
#include <iostream> #include <thread> using namespace std; 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表達式 thread t2([]() { cout << "Thread2" << endl; }); // 線程函數(shù)為函數(shù)對象 TF tf; thread t3(tf); t1.join(); t2.join(); t3.join(); cout << "Main thread!" << endl; return 0; }
效果:
thread類是防拷貝的,不允許拷貝構(gòu)造以及賦值,但是可以移動構(gòu)造和移動賦值,即將一個線程對象關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移給其他線程對象,轉(zhuǎn)移期間不影響線程的執(zhí)行
可以通過jionable()函數(shù)判斷線程是否是有效的,如果是以下任意情況,則線程無效
無效的線程: 采用無參構(gòu)造函數(shù)構(gòu)造的線程對象
線程對象的狀態(tài)已經(jīng)轉(zhuǎn)移給其他線程對象
線程已經(jīng)調(diào)用jion或者detach結(jié)束
面試題:并發(fā)與并行的區(qū)別
并發(fā)指的是多個事情,在同一時間段內(nèi)同時發(fā)生了;并行指的是多個事情,在同一時間點上同時發(fā)生了
并發(fā)的多個任務(wù)之間是互相搶占資源的;并行的多個任務(wù)之間是不互相搶占資源的,只有在多CPU的情況中才會發(fā)生并行
2、線程函數(shù)參數(shù)
線程函數(shù)的參數(shù)是以值拷貝的方式拷貝到線程??臻g中的,因此:即使線程參數(shù)為引用類型,在線程中修改后也不能修改外部實參,因為其實際引用的是線程棧中的拷貝,而不是外部實參
示例:
#include <iostream> #include <thread> using namespace std; void Func1(int& x) { x += 10; return; } void Func2(int* x) { *x += 10; return; } int main() { int a = 10; // 在線程函數(shù)中對a修改,不會影響外部實參,因為:線程函數(shù)參數(shù)雖然是引用方式,但其實際引用的是線程棧中的拷貝 // vs2019會報錯-對于引用的參數(shù)這么傳入 //thread t1(Func1, a); //t1.join(); //cout << a << endl; // 如果想要通過形參改變外部實參時,必須借助std::ref()函數(shù) thread t2(Func1, ref(a)); t2.join(); cout << a << endl; // 地址的拷貝 thread t3(Func2, &a); t3.join(); cout << a << endl; return 0; }
效果:
注意:
如果是類成員函數(shù)作為線程參數(shù)時,必須將this作為線程函數(shù)參數(shù)
示例:
#include <iostream> #include <thread> using namespace std; class A { public: void Func1(int x) { cout << x << endl; } static void Func2(int x) { cout << x << endl; } }; int main() { A a; //普通成員函數(shù)需要傳入類的實例或者指針 thread t1(&A::Func1, a, 10); t1.join(); //靜態(tài)成員函數(shù)則不用 thread t2(&A::Func2, 10); t2.join(); return 0; }
效果:
3、原子性操作庫(atomic)
多線程最主要的問題是共享數(shù)據(jù)帶來的問題(即線程安全):如果共享數(shù)據(jù)都是只讀的,那么沒問題,因為只讀操作不會影響到數(shù)據(jù),更不會涉及對數(shù)據(jù)的修改,所以所有線程都會獲得同樣的數(shù)據(jù);但是,當(dāng)一個或多個線程要修改共享數(shù)據(jù)時,就會產(chǎn)生很多潛在的麻煩
示例:
#include <iostream> #include <thread> using namespace std; unsigned long sum = 0L; void fun(size_t num) { for (size_t i = 0; i < num; ++i) sum++; } int main() { cout << "Before joining,sum = " << sum << std::endl; thread t1(fun, 10000000); thread t2(fun, 10000000); t1.join(); t2.join(); cout << "After joining,sum = " << sum << std::endl; return 0; }
效果:
C++98中傳統(tǒng)的解決方式:可以對共享修改的數(shù)據(jù)可以加鎖保護
示例:
#include <iostream> #include <thread> #include <mutex> using namespace std; std::mutex m; unsigned long sum = 0L; void fun(size_t num) { for (size_t i = 0; i < num; ++i) { m.lock(); sum++; m.unlock(); } } int main() { cout << "Before joining,sum = " << sum << std::endl; thread t1(fun, 10000000); thread t2(fun, 10000000); t1.join(); t2.join(); cout << "After joining,sum = " << sum << std::endl; return 0; }
效果:
加鎖缺陷:
只要一個線程在sum++時,其他線程就會被阻塞,會影響程序運行的效率,而且鎖如果控制不好,還容易造成死鎖
因此C++11中引入了原子操作,所謂原子操作:即不可被中斷的一個或一系列操作C++11引入的原子操作類型,使得線程間數(shù)據(jù)的同步變得非常高效
示圖:原子操作類型
注:需要使用以上原子操作變量時,必須添加頭文件#include < atomic >
示例:
#include <iostream> #include <thread> using namespace std; #include <atomic> atomic_long sum{ 0 }; void fun(size_t num) { for (size_t i = 0; i < num; ++i) sum++; // 原子操作 } int main() { cout << "Before joining, sum = " << sum << std::endl; thread t1(fun, 1000000); thread t2(fun, 1000000); t1.join(); t2.join(); cout << "After joining, sum = " << sum << std::endl; return 0; }
效果:
注意:
int main() { thread t1(fun, 1000000); thread t2(fun, 1000000); t1.join(); t2.join(); //printf("%d\n", sum);vs2019存在類型不匹配問題 //解決方式 //1. printf("%ld\n", sum.load()); //2. cout << sum << endl; //3. printf("%ld\n", (long)sum); return 0; }
atomic類模板:
在C++11中,程序員不需要對原子類型變量進行加鎖解鎖操作,線程能夠?qū)υ宇愋妥兞炕コ獾脑L問,更為普遍的,程序員可以使用atomic類模板,定義出需要的任意原子類型
atmoic<T> t; // 聲明一個類型為T的原子類型變量t
注意:
原子類型通常屬于"資源型"數(shù)據(jù),多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數(shù)中進行構(gòu)造,不允許原子類型進行拷貝構(gòu)造、移動構(gòu)造以及operator=等,為了防止意外,標準庫已經(jīng)將atmoic模板類中的拷貝構(gòu)造、移動構(gòu)造、賦值運算符重載默認刪除掉了
示例:
#include <atomic> int main() { atomic<int> a1(0); //atomic<int> a2(a1); // 編譯失敗 atomic<int> a2(0); //a2 = a1; // 編譯失敗 return 0; }
4、lock_guard與unique_lock
概念及引入:
在多線程環(huán)境下,如果想要保證某個變量的安全性,只要將其設(shè)置成對應(yīng)的原子類型即可,即高效又不容易出現(xiàn)死鎖問題
但是有些情況下,我們可能需要保證一段代碼的安全性,那么就只能通過鎖的方式來進行控制,鎖控制不好時,可能會造成死鎖 ,最常見的比如在鎖中間代碼返回,或者在鎖的范圍內(nèi)拋異常
因此:C++11采用RAII的方式對鎖進行了封裝,即lock_guard和unique_lock
1、mutex的種類
在C++11中,Mutex總共包了四個互斥量的種類: std::mutex
C++11提供的最基本的互斥量,該類的對象之間不能拷貝,也不能進行移動
mutex最常用的三個函數(shù):
函數(shù)名 | 函數(shù)功能 |
---|---|
lock() | 上鎖:鎖住互斥量 |
unlock() | 解鎖:釋放對互斥量的所有權(quán) |
try_lock() | 嘗試鎖住互斥量,如果互斥量被其他線程占有,則當(dāng)前線程也不會被阻塞 |
線程函數(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)生死鎖(deadlock)
線程函數(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)生死鎖(deadlock)
std::recursive_mutex
其允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權(quán),釋放互斥量時需要調(diào)用與該鎖層次深度相同次數(shù)的 unlock()
除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
std::timed_mutex
比 std::mutex 多了兩個成員函數(shù),try_lock_for(),try_lock_until() ,
try_lock_for()
接受一個時間范圍,表示在這一段時間范圍之內(nèi)線程如果沒有獲得鎖則被阻塞?。ㄅcstd::mutex 的 try_lock() 不同,try_lock 如果被調(diào)用時沒有獲得鎖則直接返回false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內(nèi)還是沒有獲得鎖),則返回 false
try_lock_until()接受一個時間點作為參數(shù),在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內(nèi)還是沒有獲得鎖),則返回 false
std::recursive_timed_mutex
recursive_mutex和timed_mutex的結(jié)合
2、lock_guard
std::lock_gurad 是 C++11 中定義的模板類。
定義如下:
template<class _Mutex> class lock_guard { public: // 在構(gòu)造lock_gard時上鎖 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } lock_guard(_Mutex & _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // 在析構(gòu)lock_gard時解鎖 ~lock_guard() _NOEXCEPT { _MyMutex.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex; };
解釋:
lock_guard類模板主要是通過RAII的方式,對其管理的互斥量進行了封裝,在需要加鎖的地方,只需要用上述介紹的任意互斥體實例化一個lock_guard,調(diào)用構(gòu)造函數(shù)成功上鎖,出作用域前,lock_guard對象要被銷毀,調(diào)用析構(gòu)函數(shù)自動解鎖,可以有效避免死鎖問題
lock_guard的缺陷:
太單一,用戶沒有辦法對該鎖進行控制,因此C++11又提供了unique_lock
3、unique_lock
概念及介紹:
與lock_gard類似,unique_lock類模板也是采用RAII的方式對鎖進行了封裝,并且也是以獨占所有權(quán)的方式管理mutex對象的上鎖和解鎖操作,即其對象之間不能發(fā)生拷貝
在構(gòu)造(或移動(move)賦值)時,unique_lock 對象需要傳遞一個 Mutex 對象作為它的參數(shù),新創(chuàng)建的unique_lock 對象負責(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所管理的互斥量的指針)
5、兩個線程交替打印奇數(shù)偶數(shù)
錯誤示例:使用普通的條件變量
先讓打印偶數(shù)線程獲取到所資源,然后在條件變量下等待并將鎖資源釋放
打印奇數(shù)獲取到鎖進行打印,打印后先喚醒在條件變量下等待的線程,再等待在并釋放鎖資源
再打印偶數(shù)線程被喚醒并競爭到鎖資源,進行打印…
#include <iostream> #include <thread> #include <mutex> using namespace std; int main() { int i = 1; int j = 2; bool flg = true; mutex mtx; condition_variable cv; //存在時間片切出去的問題 thread t2([&j, &mtx, &cv]() { while (j <= 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock); cout << std::this_thread::get_id() << ":" << j << endl; j += 2; cv.notify_one(); } }); thread t1([&i, &mtx, &cv]() { while (i <= 100) { std::unique_lock<mutex> lock(mtx); cout << std::this_thread::get_id() << ":" << i << endl; i += 2; cv.notify_one(); cv.wait(lock); } }); t1.join(); t2.join(); return 0; }
問題示例:
當(dāng)打印偶數(shù)線程獲取鎖后,在要等待在條件變量下之前時,時間片到了線程被切出去,再等到打印奇數(shù)線程執(zhí)行喚醒等待條件變量下的線程時沒有線程被喚醒,當(dāng)打印偶數(shù)線程時間片切回時,依舊會等待在條件變量下,而此時打印奇數(shù)線程也等待在條件變量下,此時沒人進行喚醒兩線程也就會一直進行等待
效果:
正確示例:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; int main() { int i = 1; int j = 2; bool flg = true; mutex mtx; condition_variable cv; //正確寫法 thread t1([&i, &mtx, &cv,&flg]() { while (i <= 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flg]() { return flg; });//根據(jù)條件判斷是否需要進行阻塞等待 cout << std::this_thread::get_id() << ":" << i << endl; i+=2; flg = false;//更改條件變量-使得另一個線程執(zhí)行,該線程會等待住 cv.notify_one();//進行喚醒等待條件變量下的線程 } }); thread t2([&j, &mtx, &cv, &flg]() { while (j <= 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flg]() { return !flg; }); cout << std::this_thread::get_id() << ":" << j << endl; j += 2; flg = true; cv.notify_one(); } }); t1.join(); t2.join(); return 0; }
效果:
確示例:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; int main() { int i = 1; int j = 2; bool flg = true; mutex mtx; condition_variable cv; //正確寫法 thread t1([&i, &mtx, &cv,&flg]() { while (i <= 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flg]() { return flg; });//根據(jù)條件判斷是否需要進行阻塞等待 cout << std::this_thread::get_id() << ":" << i << endl; i+=2; flg = false;//更改條件變量-使得另一個線程執(zhí)行,該線程會等待住 cv.notify_one();//進行喚醒等待條件變量下的線程 } }); thread t2([&j, &mtx, &cv, &flg]() { while (j <= 100) { std::unique_lock<mutex> lock(mtx); cv.wait(lock, [&flg]() { return !flg; }); cout << std::this_thread::get_id() << ":" << j << endl; j += 2; flg = true; cv.notify_one(); } }); t1.join(); t2.join(); return 0; }
效果:
到此這篇關(guān)于C++11 lambda表達式/包裝器/線程庫的文章就介紹到這了,更多相關(guān)C++11 lambda表達式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!