C++11-lambda表達式/包裝器/線程庫

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第15天,點擊查看活動詳情

@TOC

零、前言

本章是講解學習C++11語法新特性的第三篇文章,主要學習lambda表達式,包裝器,線程庫

一、lambda表達式

1、lambda的引入

在C++98中,如果想要對一個數據集合中的元素進行排序,可以使用std::sort方法

  • 示例:

~~~cpp

include

include

int main() { int array[] = { 4,1,8,5,3,7,0,9,2,6 }; // 默認按照小於比較,排出來結果是升序 std::sort(array, array + sizeof(array) / sizeof(array[0])); for (int i = 0; i < 10; i++) { cout << array[i] << " "; }cout << endl; // 如果需要降序,需要改變元素的比較規則 std::sort(array, array + sizeof(array) / sizeof(array[0]), greater()); for (int i = 0; i < 10; i++) { cout << array[i] << " "; }cout << endl; return 0; } ~~~

  • 效果:

image-20220509110928065

注:如果待排序元素為自定義類型,需要用户定義排序時的比較規則

  • 示例:

~~~cpp 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; } ~~~

  • 效果:

image-20220509111242047

  • 概念及引入:

隨着C++語法的發展,人們開始覺得上面的寫法太複雜了,每次為了實現一個algorithm算法, 都要重新去寫一個類,如果每次比較的邏輯不一樣,還要去實現多個類,特別是相同類的命名,這些都給編程者帶來了極大的不便。因此,在C11語法中出現了Lambda表達式

  • 示例:

~~~cpp int main() { Goods gds[] = { { "蘋果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠蘿", 1.5} }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ->bool { return l._price < r._price; }); return 0; } ~~~

注:可以看出lamb表達式實際是一個匿名函數

2、lambda表達式語法

  • lambda表達式書寫格式:

~~~cpp [capture-list] (parameters) mutable -> return-type { statement } ~~~

  • lambda表達式各部分説明:
  1. [capture-list] :

捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[]來判斷接下來的代碼是否為lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda函數使用

  1. (parameters):

參數列表,與普通函數的參數列表一致,如果不需要參數傳遞,則可以連同()一起省略

  1. mutable:

默認情況下,lambda函數總是一個const函數, mutable的作用就是讓傳值捕捉的對象可以修改,但是你修改的是傳值拷貝的對象,不影響外面對象,使用該修飾符時,參數列表不可省略(即使參數為空)

注:實際中mutable意義不大,除非你就是想傳值捕捉過來,lambda中修改,不影響外面的值

  1. ->returntype:

返回值類型,用追蹤返回類型形式聲明函數的返回值類型,沒有返回值時此部分可省略;返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導

  1. {statement}:

函數體,在該函數體內,除了可以使用其參數外,還可以使用所有捕獲到的變量

注:在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空,即C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情

  • 示例:

~~~cpp int main() { // 最簡單的lambda表達式, 該lambda表達式沒有任何意義 [] {}; // 省略參數列表和返回值類型,返回值類型由編譯器推導為int int a = 3, b = 4; [=]{ return a + 3; }; // 省略了返回值類型,無返回值類型 auto fun1 = & { b = a + c; }; fun1(10); cout << a << " " << b << endl; // 各部分都很完善的lambda函數 auto fun2 = =, &b->int {return b += a + c; }; cout << fun2(10) << endl; // 複製捕捉x int x = 10; auto add_x = xmutable { x = 2; return a + x; };//傳值捕捉修改需要mutable修飾 auto add_x1 = &x{ x = 2; return a + x; };//引用捕捉不用 cout << add_x(10) << endl; cout << x << endl; cout << add_x1(10) << endl; cout << x << endl; return 0; } ~~~

  • 效果:

image-20220512174151648

注:lambda表達式實際上可以理解為無名函數,該函數無法直接調用,如果想要直接調用,可藉助auto將其賦值給一個變量

3、捕獲列表説明

  • 概念:

捕捉列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是傳引用

  • 使用方式:

~~~cpp [var]:表示值傳遞方式捕捉變量var [=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this) [&var]:表示引用傳遞捕捉變量var [&]:表示引用傳遞捕捉所有父作用域中的變量(包括this) [this]:表示值傳遞方式捕捉當前的this指針 ~~~

  • 注意:
  1. 父作用域指包含lambda函數的語句塊

  2. 語法上捕捉列表可由多個捕捉項組成,並以逗號分割:比如:[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量 [&,a, this]:值傳遞方式捕捉變量a和this,引用方式捕捉其他變量

  3. 捕捉列表不允許變量重複傳遞,否則就會導致編譯錯誤:比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重複

  4. 在塊作用域以外的lambda函數捕捉列表必須為空;在塊作用域中的lambda函數僅能捕捉父作用域中局部變量

  5. lambda表達式之間不能相互賦值,即使看起來類型相同

  • 示例:

~~~cpp void (*PF)(); int main() { auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2; // 編譯失敗--->提示找不到operator=() // 允許使用一個lambda表達式拷貝構造一個新的副本 auto f3(f2); f3(); // 可以將沒有捕獲任何變量的lambda表達式賦值給相同類型的函數指針 PF = f2; PF(); return 0; } ~~~

  • 解釋:

Lambda是實現了函數調用運算符的匿名類(anonymous class)。對於每一個Lambda,編譯器創建匿名類,並定義相應的數據成員存儲Lambda捕獲的變量。沒有捕獲變量的Lambda不包含任何含成員變量。一個沒有任何成員變量(包括沒有虛函數表指針)的類型,在空指針上調用成員函數也不會有任何的問題,因為它的成員函數不會通過this指針訪問內存。當Lambda向函數指針的轉換時,編譯器為Lambda的匿名類實現函數指針類型轉換運算符

4、函數對象與lambda表達式

函數對象,又稱為仿函數,即可以想函數一樣使用的對象,就是在類中重載了operator()運算符的類對象

  • 示例:

~~~cpp class Rate { public: Rate(double rate): _rate(rate) {} double operator()(double money, int year) { return money * _rate * year; } private: double _rate; }; int main() { // 函數對象 double rate = 0.49; Rate r1(rate); r1(10000, 2); // lamber auto r2 = =->double{return montyrateyear; }; r2(10000, 2); return 0; } ~~~

  • 説明:

從使用方式上來看,函數對象與lambda表達式完全一樣:函數對象將rate作為其成員變量,在定義對象時給出初始值即可,lambda表達式通過捕獲列表可以直接將該變量捕獲到

  • 示圖:

image-20220509112322100

注:實際在底層編譯器對於lambda表達式的處理方式,完全就是按照函數對象的方式處理的

二、包裝器

1、function包裝器

  • 概念:

function包裝器也叫作適配器,C++中的function本質是一個類模板,也是一個包裝器

由於C++的歷史遺留問題,導致如果想實現一個函數功能,可以採用函數名、函數指針、仿函數、有名稱的lambda表達式,所有這些都是可調用的類型

  • 存在的問題:
  1. 函數指針類型太複雜,不方便使用和理解
  2. 仿函數類型是一個類名,沒有指定調用參數和返回值,得去看operator()的實現才能看出來
  3. lambda表達式在語法層,看不到類型,只能在底層看到其類型,基本都是lambda_uuid
  • 示例:

~~~cpp template 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() { // 函數名 cout << useF(f, 11.11) << endl; // 函數對象 cout << useF(Functor(), 11.11) << endl; // lamber表達式 cout << useF(->double { return d / 4; }, 11.11) << endl; return 0; } ~~~

  • 效果:

image-20220512181306891

注:對於函數名稱,仿函數對象,lambda表達式對象這些都是可調用的類型,我們發現發現useF函數模板實例化了三份,所以如此豐富的類型,可能會導致模板的效率低下,包裝器可以很好的解決該問題

  • 包裝器原型:

~~~cpp // 類模板原型如下 template function; // undefined template class function; ~~~

模板參數説明:

Ret: 被調用函數的返回類型

Args…:被調用函數的形參

注:std::function在頭文件< functional >

示例:

~~~cpp

include

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() { // 函數名(函數指針) std::function func1 = f; cout << func1(1, 2) << endl; // 函數對象 std::function func2 = Functor(); cout << func2(1, 2) << endl; // lamber表達式 std::function func3 = { return a + b; }; cout << func3(1, 2) << endl; // 類的成員函數 std::function func4 = Plus::plusi; cout << func4(1, 2) << endl; std::function func5 = &Plus::plusd;//對於普通成員的包裝一定要加上&,需要通過指針進行調用成員函數 cout << func5(Plus(), 1.1, 2.2) << endl;//傳入類對象,通過對象進行調用 return 0; } ~~~

  • 效果:

image-20220512230505507

  • 包裝器解決模板實例化多份的問題:

~~~cpp

include

template 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() { //將多個可調用類型進行封裝成相同類型,便於統一調用 // 函數名 std::function func1 = f; cout << useF(func1, 11.11) << endl; // 函數對象 std::function func2 = Functor(); cout << useF(func2, 11.11) << endl; // lamber表達式 std::function func3 = ->double { return d /4; }; cout << useF(func3, 11.11) << endl; return 0; } ~~~

  • 效果:

image-20220512230833720

2、bind

  • 概念:
  1. std::bind函數定義在頭文件中,是一個函數模板,它就像一個函數包裝器(適配器),接受一個可調用對象(callable object),生成一個新的可調用對象來“適應”原對象的參數列表
  2. 一般而言,我們用它可以把一個原本接收N個參數的函數fn,通過綁定一些參數,返回一個接收M個(M可以大於N,但這麼做沒什麼意義)參數的新函數;同時,使用std::bind函數還可以實現參數順序調整等操作
  • 示例:

~~~cpp

include

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() { //普通函數的綁定 //表示綁定函數plus 參數分別由調用 func1 的第一,二個參數指定(placeholders用來表示參數位佔位) std::function func1 = std::bind(Plus, placeholders::_1,placeholders::_2); //auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);直接使用auto識別類型

//表示綁定函數 plus 的第一,二參數為: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2(2,3) << endl;//func2();也可以不用傳參數-因為參數已經綁定好了,傳入的參數沒有實際的作用

//類函數的綁定
//類的成員函數必須通過類的對象或者指針調用,因此在bind時,bind的第一個參數的位置來指定一個類的實列、指針或引用。
Sub s;
// 綁定成員函數
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2);
// 參數調換順序
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);//靜態成員函數的綁定-不需要類的示例指針或引用
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
cout << func5(1, 2) << endl;
return 0;

} ~~~

  • 效果:

image-20220513103146147

  • 總結:

bind是對包裝的可調用類型的進一步封裝,可以根據自己的需要進行調整參數的數據及位置,綁定類對象能有優化成員函數的包裝使用,更加符合使用習慣

三、線程庫

1、線程的概念及使用

  • thread類的簡單介紹:
  1. 在C++11之前,涉及到多線程問題,都是和平台相關的,比如windows和linux下各有自己的接口,這使得代碼的可移植性比較差
  2. C++11中最重要的特性就是對線程進行支持了,使得C++在並行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念

注:要使用標準庫中的線程,必須包含< thread >頭文件

  • 線程常用接口:

| 函數名 | 功能 | | ----------------------------- | ------------------------------------------------------------ | | thread() | 構造一個線程對象,沒有關聯任何線程函數,即沒有啟動任何線程 | | thread(fn, args1, args2, ...) | 構造一個線程對象,並關聯線程函數fn,args1,args2,...為線程函數的 參數 | | get_id() | 獲取線程id | | jionable() | 線程是否還在執行,joinable代表的是一個正在執行中的線程。 | | jion() | 該函數調用後會阻塞住線程,當該線程結束後,主線程繼續執行 | | detach() | 在創建線程對象後馬上調用,用於把被創建線程與線程對象分離開,分離 的線程變為後台線程,創建的線程的"死活"就與主線程無關 |

  • 注意:
  1. 線程是操作系統中的一個概念,是進程中的一個執行分支,線程對象可以關聯一個線程,用來控制線程以及獲取線程的狀態

  2. 當創建一個線程對象後,沒有提供線程函數,該對象實際沒有對應任何線程

  • 示例:

~~~cpp

include

int main() { std::thread t1;//空線程 cout << t1.get_id() << endl; return 0; } ~~~

注:get_id()的返回值類型為id類型,id類型實際為std::thread命名空間下封裝的一個類,該類中包含了一個結構體

  • 對應結構體的定義:

~~~cpp // vs下查看 typedef struct { / thread identifier for Win32 / void _Hnd; / Win32 HANDLE */ unsigned int _Id; } _Thrd_imp_t; ~~~

  1. 當創建一個線程對象後,並且給線程關聯線程函數,該線程就被啟動,與主線程一起運行
  • 線程函數一般情況下可按照以下三種方式提供:

  • 函數指針

  • lambda表達式
  • 函數對象
  • 示例:

~~~cpp

include

include

using namespace std;

void ThreadFunc(int a) { cout << "Thread1" << a << endl; } class TF { public: void operator()() { cout << "Thread3" << endl; } }; 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; } ~~~

  • 效果:

image-20220513104734755

  1. thread類是防拷貝的,不允許拷貝構造以及賦值,但是可以移動構造和移動賦值,即將一個線程對象關聯線程的狀態轉移給其他線程對象,轉移期間不影響線程的執行

可以通過jionable()函數判斷線程是否是有效的,如果是以下任意情況,則線程無效

  • 無效的線程:

  • 採用無參構造函數構造的線程對象

  • 線程對象的狀態已經轉移給其他線程對象
  • 線程已經調用jion或者detach結束
  • 面試題:併發與並行的區別
  1. 併發指的是多個事情,在同一時間段內同時發生了;並行指的是多個事情,在同一時間點上同時發生了
  2. 併發的多個任務之間是互相搶佔資源的;並行的多個任務之間是不互相搶佔資源的,只有在多CPU的情況中才會發生並行

2、線程函數參數

線程函數的參數是以值拷貝的方式拷貝到線程棧空間中的,因此:即使線程參數為引用類型,在線程中修改後也不能修改外部實參,因為其實際引用的是線程棧中的拷貝,而不是外部實參

  • 示例:

~~~cpp

include

include

using namespace std; void Func1(int& x) { x += 10; return; } void Func2(int x) { x += 10; return; } int main() { int a = 10; // 在線程函數中對a修改,不會影響外部實參,因為:線程函數參數雖然是引用方式,但其實際引用的是線程棧中的拷貝 // vs2019會報錯-對於引用的參數這麼傳入 //thread t1(Func1, a); //t1.join(); //cout << a << endl; // 如果想要通過形參改變外部實參時,必須藉助std::ref()函數 thread t2(Func1, ref(a)); t2.join(); cout << a << endl; // 地址的拷貝 thread t3(Func2, &a); t3.join(); cout << a << endl; return 0; } ~~~

  • 效果:

image-20220513110911773

  • 注意:

如果是類成員函數作為線程參數時,必須將this作為線程函數參數

  • 示例:

~~~cpp

include

include

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; //普通成員函數需要傳入類的實例或者指針 thread t1(&A::Func1, a, 10); t1.join(); //靜態成員函數則不用 thread t2(&A::Func2, 10); t2.join(); return 0; } ~~~

  • 效果:

image-20220513112613597

3、原子性操作庫(atomic)

多線程最主要的問題是共享數據帶來的問題(即線程安全):如果共享數據都是隻讀的,那麼沒問題,因為只讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據;但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩

  • 示例:

~~~cpp

include

include

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; } ~~~

  • 效果:

image-20220513112906361

C++98中傳統的解決方式:可以對共享修改的數據可以加鎖保護

  • 示例:

~~~cpp

include

include

include

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; } ~~~

  • 效果:

image-20220513113456205

  • 加鎖缺陷:

只要一個線程在sum++時,其他線程就會被阻塞,會影響程序運行的效率,而且鎖如果控制不好,還容易造成死鎖

因此C++11中引入了原子操作,所謂原子操作:即不可被中斷的一個或一系列操作C++11引入的原子操作類型,使得線程間數據的同步變得非常高效

  • 示圖:原子操作類型

image-20220513113538668

注:需要使用以上原子操作變量時,必須添加頭文件#include < atomic >

  • 示例:

~~~cpp

include

include

using namespace std;

include

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; } ~~~

  • 效果:

image-20220513113742588

  • 注意:

~~~cpp 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中,程序員不需要對原子類型變量進行加鎖解鎖操作,線程能夠對原子類型變量互斥的訪問,更為普遍的,程序員可以使用atomic類模板,定義出需要的任意原子類型

~~~cpp atmoic t; // 聲明一個類型為T的原子類型變量t ~~~

  • 注意:

原子類型通常屬於"資源型"數據,多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數中進行構造,不允許原子類型進行拷貝構造、移動構造以及operator=等,為了防止意外,標準庫已經將atmoic模板類中的拷貝構造、移動構造、賦值運算符重載默認刪除掉了

  • 示例:

~~~cpp

include

int main() { atomic a1(0); //atomic a2(a1); // 編譯失敗 atomic a2(0); //a2 = a1; // 編譯失敗 return 0; } ~~~

4、lock_guard與unique_lock

  • 概念及引入:
  1. 在多線程環境下,如果想要保證某個變量的安全性,只要將其設置成對應的原子類型即可,即高效又不容易出現死鎖問題

  2. 但是有些情況下,我們可能需要保證一段代碼的安全性,那麼就只能通過鎖的方式來進行控制,鎖控制不好時,可能會造成死鎖 ,最常見的比如在鎖中間代碼返回,或者在鎖的範圍內拋異常

  3. 因此:C++11採用RAII的方式對鎖進行了封裝,即lock_guard和unique_lock

1、mutex的種類

  • 在C++11中,Mutex總共包了四個互斥量的種類:

  • std::mutex

C++11提供的最基本的互斥量,該類的對象之間不能拷貝,也不能進行移動

  • mutex最常用的三個函數:

| 函數名 | 函數功能 | | ---------- | ------------------------------------------------------------ | | lock() | 上鎖:鎖住互斥量 | | unlock() | 解鎖:釋放對互斥量的所有權 | | try_lock() | 嘗試鎖住互斥量,如果互斥量被其他線程佔有,則當前線程也不會被阻塞 |

  • 線程函數調用lock()時可能會發生以下三種情況:
  1. 如果該互斥量當前沒有被鎖住,則調用線程將該互斥量鎖住,直到調用 unlock之前,該線程一直擁有該鎖
  2. 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住
  3. 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)
  • 線程函數調用try_lock()時可能會發生以下三種情況:
  1. 如果當前互斥量沒有被其他線程佔有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量
  2. 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而並不會被阻塞掉
  3. 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)
  1. std::recursive_mutex
  1. 其允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock()
  2. 除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
  1. std::timed_mutex
  1. 比 std::mutex 多了兩個成員函數,try_lock_for(),try_lock_until() , try_lock_for()
  2. 接受一個時間範圍,表示在這一段時間範圍之內線程如果沒有獲得鎖則被阻塞住(與std::mutex 的 try_lock() 不同,try_lock 如果被調用時沒有獲得鎖則直接返回false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false
  3. try_lock_until()接受一個時間點作為參數,在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false
  1. std::recursive_timed_mutex

recursive_mutex和timed_mutex的結合

1、lock_guard

std::lock_gurad 是 C++11 中定義的模板類。

定義如下:

~~~cpp template class lock_guard { public: // 在構造lock_gard時上鎖 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } lock_guard(_Mutex & _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // 在析構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,調用構造函數成功上鎖,出作用域前,lock_guard對象要被銷燬,調用析構函數自動解鎖,可以有效避免死鎖問題

  • lock_guard的缺陷:

太單一,用户沒有辦法對該鎖進行控制,因此C++11又提供了unique_lock

3、unique_lock

  • 概念及介紹:
  1. 與lock_gard類似,unique_lock類模板也是採用RAII的方式對鎖進行了封裝,並且也是以獨佔所有權的方式管理mutex對象的上鎖和解鎖操作,即其對象之間不能發生拷貝
  2. 在構造(或移動(move)賦值)時,unique_lock 對象需要傳遞一個 Mutex 對象作為它的參數,新創建的unique_lock 對象負責傳入的 Mutex 對象的上鎖和解鎖操作。使用以上類型互斥量實例化unique_lock的對象時,自動調用構造函數上鎖,unique_lock對象銷燬時自動調用析構函數解鎖,可以很方便的防止死鎖問題
  3. 與lock_guard不同的是,unique_lock更加的靈活,提供了更多的成員函數: 上鎖/解鎖操作:lock、try_lock、try_lock_for、try_lock_until和unlock 修改操作:移動賦值、交換(swap:與另一個unique_lock對象互換所管理的互斥量所有權)、釋放(release:返回它所管理的互斥量對象的指針,並釋放所有權) 獲取屬性:owns_lock(返回當前對象是否上了鎖)、operator bool()(與owns_lock()的功能相同)、mutex(返回當前unique_lock所管理的互斥量的指針)

5、兩個線程交替打印奇數偶數

  • 錯誤示例:使用普通的條件變量
  1. 先讓打印偶數線程獲取到所資源,然後在條件變量下等待並將鎖資源釋放
  2. 打印奇數獲取到鎖進行打印,打印後先喚醒在條件變量下等待的線程,再等待在並釋放鎖資源
  3. 再打印偶數線程被喚醒並競爭到鎖資源,進行打印...

~~~cpp

include

include

include

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 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 lock(mtx); cout << std::this_thread::get_id() << ":" << i << endl; i += 2; cv.notify_one(); cv.wait(lock); } }); t1.join(); t2.join(); return 0; } ~~~

  • 問題示例:

當打印偶數線程獲取鎖後,在要等待在條件變量下之前時,時間片到了線程被切出去,再等到打印奇數線程執行喚醒等待條件變量下的線程時沒有線程被喚醒,當打印偶數線程時間片切回時,依舊會等待在條件變量下,而此時打印奇數線程也等待在條件變量下,此時沒人進行喚醒兩線程也就會一直進行等待

  • 效果:

image-20220514204054462

  • 正確示例:

~~~cpp

include

include

include

include

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 lock(mtx); cv.wait(lock, &flg { return flg; });//根據條件判斷是否需要進行阻塞等待 cout << std::this_thread::get_id() << ":" << i << endl; i+=2; flg = false;//更改條件變量-使得另一個線程執行,該線程會等待住 cv.notify_one();//進行喚醒等待條件變量下的線程 } }); thread t2(&j, &mtx, &cv, &flg { while (j <= 100) { std::unique_lock 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; } ~~~

  • 效果:

image-20220514204437521

確示例:

~~~cpp

include

include

include

include

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 lock(mtx); cv.wait(lock, &flg { return flg; });//根據條件判斷是否需要進行阻塞等待 cout << std::this_thread::get_id() << ":" << i << endl; i+=2; flg = false;//更改條件變量-使得另一個線程執行,該線程會等待住 cv.notify_one();//進行喚醒等待條件變量下的線程 } }); thread t2(&j, &mtx, &cv, &flg { while (j <= 100) { std::unique_lock 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; } ~~~

  • 效果:

image-20220514204437521