亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲(chóng)蟲(chóng)首頁(yè)| 資源下載| 資源專(zhuān)輯| 精品軟件
登錄| 注冊(cè)

您現(xiàn)在的位置是:首頁(yè) > 技術(shù)閱讀 >  C++并發(fā)編程(C++11到C++17)

C++并發(fā)編程(C++11到C++17)

時(shí)間:2024-02-12

置頂/星標(biāo)公眾號(hào)????,硬核文章第一時(shí)間送達(dá)!


為什么要并發(fā)編程


大型的軟件項(xiàng)目常常包含非常多的任務(wù)需要處理。例如:對(duì)于大量數(shù)據(jù)的數(shù)據(jù)流處理,或者是包含復(fù)雜GUI界面的應(yīng)用程序。如果將所有的任務(wù)都以串行的方式執(zhí)行,則整個(gè)系統(tǒng)的效率將會(huì)非常低下,應(yīng)用程序的用戶(hù)體驗(yàn)會(huì)非常的差。

另一方面,自上個(gè)世紀(jì)六七十年代英特爾創(chuàng)始人之一 Gordon Moore 提出 摩爾定義 以來(lái),CPU頻率以每18個(gè)月翻一番的指數(shù)速度增長(zhǎng)。但這一增長(zhǎng)在最近的十年已經(jīng)基本停滯,大家會(huì)發(fā)現(xiàn)曾經(jīng)有過(guò)一段時(shí)間CPU的頻率從3G到達(dá)4G,但在這之后就停滯不前了。因此最近的新款CPU也基本上都是3G左右的頻率。相應(yīng)的,CPU以更多核的形式在增長(zhǎng)。目前的Intel i7有8核的版本,Xeon處理器達(dá)到了28核。并且,最近幾年手機(jī)上使用的CPU也基本上是4核或者8核的了。

由此,掌握并發(fā)編程技術(shù),利用多處理器來(lái)提升軟件項(xiàng)目的性能將是軟件工程師的一項(xiàng)基本技能。

本文以C++語(yǔ)言為例,講解如何進(jìn)行并發(fā)編程。并盡可能涉及C++11,C++14以及C++17中的主要內(nèi)容。

并發(fā)與并行


并發(fā)(Concurrent)與并行(Parallel)都是很常見(jiàn)的術(shù)語(yǔ)。

Erlang之父Joe Armstrong曾經(jīng)以人們使用咖啡機(jī)的場(chǎng)景為例描述了這兩個(gè)術(shù)語(yǔ)。如下圖所示:


  • 并發(fā):如果多個(gè)隊(duì)列可以交替使用某臺(tái)咖啡機(jī),則這一行為就是并發(fā)的。
  • 并行:如果存在多臺(tái)咖啡機(jī)可以被多個(gè)隊(duì)列交替使用,則就是并行。

這里隊(duì)列中的每個(gè)人類(lèi)比于計(jì)算機(jī)的任務(wù),咖啡機(jī)類(lèi)比于計(jì)算機(jī)處理器。因此:并發(fā)和并行都是在多任務(wù)的環(huán)境下的討論。

更嚴(yán)格的來(lái)說(shuō):如果一個(gè)系統(tǒng)支持多個(gè)動(dòng)作同時(shí)存在,那么這個(gè)系統(tǒng)就是一個(gè)并發(fā)系統(tǒng)。如果這個(gè)系統(tǒng)還支持多個(gè)動(dòng)作(物理時(shí)間上)同時(shí)執(zhí)行,那么這個(gè)系統(tǒng)就是一個(gè)并行系統(tǒng)。

你可能已經(jīng)看出,“并行”其實(shí)是“并發(fā)”的子集。它們的區(qū)別在于是否具有多個(gè)處理器。如果存在多個(gè)處理器同時(shí)執(zhí)行多個(gè)線程,就是并行。

在不考慮處理器數(shù)量的情況下,我們統(tǒng)稱(chēng)之為“并發(fā)”。

進(jìn)程與線程


進(jìn)程與線程是操作系統(tǒng)的基本概念。無(wú)論是桌面系統(tǒng):MacOS,Linux,Windows,還是移動(dòng)操作系統(tǒng):Android,iOS,都存在進(jìn)程和線程的概念。

進(jìn)程(英語(yǔ):process),是指計(jì)算機(jī)中已運(yùn)行的程序。進(jìn)程為曾經(jīng)是分時(shí)系統(tǒng)的基本運(yùn)作單位。在面向進(jìn)程設(shè)計(jì)的系統(tǒng)(如早期的UNIX,Linux 2.4及更早的版本)中,進(jìn)程是程序的基本執(zhí)行實(shí)體;
線程(英語(yǔ):thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。
-- 維基百科

關(guān)于這兩個(gè)概念在任何一本操作系統(tǒng)書(shū)上都可以找到定義。網(wǎng)上也有很多文章對(duì)它們進(jìn)行了解釋。因此這里不再贅述,這里僅僅提及一下它們與編程的關(guān)系。

對(duì)于絕大部分編程語(yǔ)言或者編程環(huán)境來(lái)說(shuō),我們所寫(xiě)的程序都會(huì)在一個(gè)進(jìn)程中運(yùn)行。一個(gè)進(jìn)程至少會(huì)包含一個(gè)線程。這個(gè)線程我們通常稱(chēng)之為主線程。

在默認(rèn)的情況下,我們寫(xiě)的代碼都是在進(jìn)程的主線程中運(yùn)行,除非開(kāi)發(fā)者在程序中創(chuàng)建了新的線程。

不同編程語(yǔ)言的線程環(huán)境會(huì)不一樣,Java語(yǔ)言在很早就支持了多線程接口。(Java程序在Java虛擬機(jī)中運(yùn)行,虛擬機(jī)通常還會(huì)包含自己特有的線程,例如垃圾回收線程。)。而對(duì)于JavaScript這樣的語(yǔ)言來(lái)說(shuō),它就沒(méi)有多線程的概念。

當(dāng)我們只有一個(gè)處理器時(shí),所有的進(jìn)程或線程會(huì)分時(shí)占用這個(gè)處理器。但如果系統(tǒng)中存在多個(gè)處理器時(shí),則就可能有多個(gè)任務(wù)并行的運(yùn)行在不同的處理器上。

下面兩幅圖以不同顏色的矩形代表不同的任務(wù)(可能是進(jìn)程,也可能是線程)來(lái)描述它們可能在處理器上執(zhí)行的順序。

下圖是單核處理器的情況:


下面是四核處理器的情況:


任務(wù)會(huì)在何時(shí)占有處理器,通常是由操作系統(tǒng)的調(diào)度策略決定的。在《Android系統(tǒng)上的進(jìn)程管理:進(jìn)程的調(diào)度》一文中,我們介紹過(guò)Linux的調(diào)度策略。

當(dāng)我們?cè)陂_(kāi)發(fā)跨平臺(tái)的軟件時(shí),我們不應(yīng)當(dāng)對(duì)調(diào)度策略做任何假設(shè),而應(yīng)該抱有“系統(tǒng)可能以任意順序來(lái)調(diào)度我的任務(wù)”這樣的想法。

并發(fā)系統(tǒng)的性能


開(kāi)發(fā)并發(fā)系統(tǒng)最主要的動(dòng)機(jī)就是提升系統(tǒng)性能(事實(shí)上,這是以增加復(fù)雜度為代價(jià)的)。

但我們需要知道,單純的使用多線程并不一定能提升系統(tǒng)性能(當(dāng)然,也并非線程越多系統(tǒng)的性能就越好)。從上面的兩幅圖我們就可以直觀的感受到:線程(任務(wù))的數(shù)量要根據(jù)具體的處理器數(shù)量來(lái)決定。假設(shè)只有一個(gè)處理器,那么劃分太多線程可能會(huì)適得其反。因?yàn)楹芏鄷r(shí)間都花在任務(wù)切換上了。

因此,在設(shè)計(jì)并發(fā)系統(tǒng)之前,一方面我們需要做好對(duì)于硬件性能的了解,另一方面需要對(duì)我們的任務(wù)有足夠的認(rèn)識(shí)。

關(guān)于這一點(diǎn),你可能需要了解一下阿姆達(dá)爾定律了。對(duì)于這個(gè)定律,簡(jiǎn)單來(lái)說(shuō):我們想要預(yù)先意識(shí)到那些任務(wù)是可以并行的,那些是無(wú)法并行的。只有明確了任務(wù)的性質(zhì),才能有的放矢的進(jìn)行優(yōu)化。這個(gè)定律告訴了我們將系統(tǒng)并行之后性能收益的上限。

關(guān)于阿姆達(dá)爾定律在Linux系統(tǒng)監(jiān)測(cè)工具sysstat介紹一文中已經(jīng)介紹過(guò),因此這里不再贅述。

C++與并發(fā)編程


前面我們已經(jīng)了解到,并非所有的語(yǔ)言都提供了多線程的環(huán)境。

即便是C++語(yǔ)言,直到C++11標(biāo)準(zhǔn)之前,也是沒(méi)有多線程支持的。在這種情況下,Linux/Unix平臺(tái)下的開(kāi)發(fā)者通常會(huì)使用POSIX Threads,Windows上的開(kāi)發(fā)者也會(huì)有相應(yīng)的接口。但很明顯,這些API都只針對(duì)特定的操作系統(tǒng)平臺(tái),可移植性較差。如果要同時(shí)支持Linux和Windows系統(tǒng),你可能要寫(xiě)兩套代碼。

相較而言,Java自JDK 1.0就包含了多線程模型。

這個(gè)狀態(tài)在C++ 11標(biāo)準(zhǔn)發(fā)布之后得到了改變。并且,在C++ 14和C++ 17標(biāo)準(zhǔn)中又對(duì)并發(fā)編程機(jī)制進(jìn)行了增強(qiáng)。

下圖是最近幾個(gè)版本的C++標(biāo)準(zhǔn)特性的線路圖。


編譯器與C++標(biāo)準(zhǔn)


編譯器對(duì)于語(yǔ)言特性的支持是逐步完成的。想要使用特定的特性你需要相應(yīng)版本的編譯器。

  • GCC對(duì)于C++特性的支持請(qǐng)參見(jiàn)這里:C++ Standards Support in GCC。
  • Clang對(duì)于C++特性的支持請(qǐng)參見(jiàn)這里:C++ Support in Clang。

下面兩個(gè)表格列出了C++標(biāo)準(zhǔn)和相應(yīng)編譯器的版本對(duì)照:

  • C++標(biāo)準(zhǔn)與相應(yīng)的GCC版本要求如下:
  • C++標(biāo)準(zhǔn)與相應(yīng)的Clang版本要求如下:

默認(rèn)情況下編譯器是以較低的標(biāo)準(zhǔn)來(lái)進(jìn)行編譯的,如果希望使用新的標(biāo)準(zhǔn),你需要通過(guò)編譯參數(shù)-std=c++xx告知編譯器,例如:

g++ -std=c++17 your_file.cpp -o your_program

測(cè)試環(huán)境


本文的源碼可以到下載我的github上獲取,地址:paulQuei/cpp-concurrency。
你可以直接通過(guò)下面這條命令獲取源碼:

git clone https://github.com/paulQuei/cpp-concurrency.git

源碼下載之后,你可以通過(guò)任何文本編輯器瀏覽源碼。如果希望編譯和運(yùn)行程序,你還需要按照下面的內(nèi)容來(lái)準(zhǔn)備環(huán)境。

本文中的源碼使用cmake編譯,只有cmake 3.8以上的版本才支持C++ 17,所以你需要安裝這個(gè)或者更新版本的cmake。

另外,截止目前(2019年10月)為止,clang編譯器還不支持并行算法。

但是gcc-9是支持的。因此想要編譯和運(yùn)行這部分代碼,你需要安裝gcc 9.0或更新的版本。并且,gcc-9還要依賴(lài)Intel Threading Building Blocks才能使用并行算法以及<execution>頭文件。

具體的安裝方法見(jiàn)下文。

具體編譯器對(duì)于C++特性支持的情況請(qǐng)參見(jiàn)這里:C++ compiler support。

安裝好之后運(yùn)行根目錄下的下面這個(gè)命令即可:
 

./make_all.sh

它會(huì)完成所有的編譯工作。

本文的源碼在下面兩個(gè)環(huán)境中經(jīng)過(guò)測(cè)試,環(huán)境的準(zhǔn)備方法如下。

MacOS


在Mac上,我使用brew工具安裝gcc以及tbb庫(kù)。

考慮到其他人與我的環(huán)境可能會(huì)有所差異,所以需要手動(dòng)告知tbb庫(kù)的安裝路徑。
讀者需要執(zhí)行下面這些命令來(lái)準(zhǔn)備環(huán)境:


rew install gccbrew insbtall tbb
export tbb_path=/usr/local/Cellar/tbb/2019_U8/./make_all.sh

注意,請(qǐng)通過(guò)運(yùn)行g(shù)++-9命令以確認(rèn)gcc的版本是否正確,如果版本較低,則需要通過(guò)brew命令將其升級(jí)到新版本:

brew upgrade gcc

Ubuntu


Ubuntu上,通過(guò)下面的命令安裝gcc-9。

sudo add-apt-repository ppa:ubuntu-toolchain-r/testsudo apt-get updatesudo apt install gcc-9 g++-9

但安裝tbb庫(kù)就有些麻煩了。這是因?yàn)閁buntu 16.04默認(rèn)關(guān)聯(lián)的版本是較低的,直接安裝是無(wú)法使用的。我們需要安裝更新的版本。
聯(lián)網(wǎng)安裝的方式步驟繁瑣,所以可以通過(guò)下載包的方式進(jìn)行安裝,我已經(jīng)將這需要的兩個(gè)文件放到的這里:

  • libtbb2_2019~U8-1_amd64.deb
  • libtbb-dev_2019~U8-1_amd64.deb

如果需要,你可以下載后通過(guò)apt命令安裝即可:

sudo apt install ~/Downloads/libtbb2_2019~U8-1_amd64.deb sudo apt install ~/Downloads/libtbb-dev_2019~U8-1_amd64.deb

線程


創(chuàng)建線程


創(chuàng)建線程非常的簡(jiǎn)單的,下面就是一個(gè)使用了多線程的Hello World示例:

// 01_hello_thread.cpp
#include <iostream>#include <thread> // ①
using namespace std; // ②
void hello() { // ③ cout << "Hello World from new thread." << endl;}
int main() { thread t(hello); // ④ t.join(); // ⑤
return 0;}

對(duì)于這段代碼說(shuō)明如下:

  1. 為了使用多線程的接口,我們需要#include <thread>頭文件。
  2. 為了簡(jiǎn)化聲明,本文中的代碼都將using namespace std;。
  3. 新建線程的入口是一個(gè)普通的函數(shù),它并沒(méi)有什么特別的地方。
  4. 創(chuàng)建線程的方式就是構(gòu)造一個(gè)thread對(duì)象,并指定入口函數(shù)。與普通對(duì)象不一樣的是,此時(shí)編譯器便會(huì)為我們創(chuàng)建一個(gè)新的操作系統(tǒng)線程,并在新的線程中執(zhí)行我們的入口函數(shù)。
  5. 關(guān)于join函數(shù)在下文中講解。

thread可以和callable類(lèi)型一起工作,因此如果你熟悉lambda表達(dá)式,你可以直接用它來(lái)寫(xiě)線程的邏輯,像這樣:

// 02_lambda_thread.cpp
#include <iostream>#include <thread>
using namespace std;
int main() { thread t([] { cout << "Hello World from lambda thread." << endl; });
t.join();
return 0;}

為了減少不必要的重復(fù),若無(wú)必要,下文中的代碼將不貼出include指令以及using聲明。

當(dāng)然,你可以傳遞參數(shù)給入口函數(shù),像下面這樣:

// 03_thread_argument.cpp
void hello(string name) { cout << "Welcome to " << name << endl;}
int main() { thread t(hello, "https://paul.pub"); t.join();
return 0;}

不過(guò)需要注意的是,參數(shù)是以拷貝的形式進(jìn)行傳遞的。因此對(duì)于拷貝耗時(shí)的對(duì)象你可能需要傳遞指針或者引用類(lèi)型作為參數(shù)。但是,如果是傳遞指針或者引用,你還需要考慮參數(shù)對(duì)象的生命周期。因?yàn)榫€程的運(yùn)行長(zhǎng)度很可能會(huì)超過(guò)參數(shù)的生命周期(見(jiàn)下文detach),這個(gè)時(shí)候如果線程還在訪問(wèn)一個(gè)已經(jīng)被銷(xiāo)毀的對(duì)象就會(huì)出現(xiàn)問(wèn)題。

join與detach


  • 主要API

一旦啟動(dòng)線程之后,我們必須決定是要等待直接它結(jié)束(通過(guò)join),還是讓它獨(dú)立運(yùn)行(通過(guò)detach),我們必須二者選其一。如果在thread對(duì)象銷(xiāo)毀的時(shí)候我們還沒(méi)有做決定,則thread對(duì)象在析構(gòu)函數(shù)出將調(diào)用std::terminate()從而導(dǎo)致我們的進(jìn)程異常退出。

請(qǐng)思考在上面的代碼示例中,thread對(duì)象在何時(shí)會(huì)銷(xiāo)毀。

需要注意的是:在我們做決定的時(shí)候,很可能線程已經(jīng)執(zhí)行完了(例如上面的示例中線程的邏輯僅僅是一句打印,執(zhí)行時(shí)間會(huì)很短)。新的線程創(chuàng)建之后,究竟是新的線程先執(zhí)行,還是當(dāng)前線程的下一條語(yǔ)句先執(zhí)行這是不確定的,因?yàn)檫@是由操作系統(tǒng)的調(diào)度策略決定的。不過(guò)這不要緊,我們只要在thread對(duì)象銷(xiāo)毀前做決定即可。

  • join:調(diào)用此接口時(shí),當(dāng)前線程會(huì)一直阻塞,直到目標(biāo)線程執(zhí)行完成(當(dāng)然,很可能目標(biāo)線程在此處調(diào)用之前就已經(jīng)執(zhí)行完成了,不過(guò)這不要緊)。因此,如果目標(biāo)線程的任務(wù)非常耗時(shí),你就要考慮好是否需要在主線程上等待它了,因此這很可能會(huì)導(dǎo)致主線程卡住。
  • detach:detach是讓目標(biāo)線程成為守護(hù)線程(daemon threads)。一旦detach之后,目標(biāo)線程將獨(dú)立執(zhí)行,即便其對(duì)應(yīng)的thread對(duì)象銷(xiāo)毀也不影響線程的執(zhí)行。并且,你無(wú)法再與之通信。

對(duì)于這兩個(gè)接口,都必須是可執(zhí)行的線程才有意義。你可以通過(guò)joinable()接口查詢(xún)是否可以對(duì)它們進(jìn)行join或者detach。

管理當(dāng)前線程


  • 主要API

上面是一些在線程內(nèi)部使用的API,它們用來(lái)對(duì)當(dāng)前線程做一些控制。

  • yield 通常用在自己的主要任務(wù)已經(jīng)完成的時(shí)候,此時(shí)希望讓出處理器給其他任務(wù)使用。
  • get_id 返回當(dāng)前線程的id,可以以此來(lái)標(biāo)識(shí)不同的線程。
  • sleep_for 是讓當(dāng)前線程停止一段時(shí)間。
  • sleep_until 和sleep_for類(lèi)似,但是是以具體的時(shí)間點(diǎn)為參數(shù)。這兩個(gè)API都以chrono API(由于篇幅所限,這里不展開(kāi)這方面內(nèi)容)為基礎(chǔ)。

下面是一個(gè)代碼示例:

// 04_thread_self_manage.cpp
void print_time() { auto now = chrono::system_clock::now(); auto in_time_t = chrono::system_clock::to_time_t(now);
std::stringstream ss; ss << put_time(localtime(&in_time_t), "%Y-%m-%d %X"); cout << "now is: " << ss.str() << endl;}
void sleep_thread() { this_thread::sleep_for(chrono::seconds(3)); cout << "[thread-" << this_thread::get_id() << "] is waking up" << endl;}
void loop_thread() { for (int i = 0; i < 10; i++) { cout << "[thread-" << this_thread::get_id() << "] print: " << i << endl; }}
int main() { print_time();
thread t1(sleep_thread); thread t2(loop_thread);
t1.join(); t2.detach();
print_time(); return 0;}

這段代碼應(yīng)該還是比較容易理解的,這里創(chuàng)建了兩個(gè)線程。它們都會(huì)有一些輸出,其中一個(gè)會(huì)先停止3秒鐘,然后再輸出。主線程調(diào)用join會(huì)一直卡住等待它運(yùn)行結(jié)束。
這段程序的輸出如下:

now is: 2019-10-13 10:17:48[thread-0x70000cdda000] print: 0[thread-0x70000cdda000] print: 1[thread-0x70000cdda000] print: 2[thread-0x70000cdda000] print: 3[thread-0x70000cdda000] print: 4[thread-0x70000cdda000] print: 5[thread-0x70000cdda000] print: 6[thread-0x70000cdda000] print: 7[thread-0x70000cdda000] print: 8[thread-0x70000cdda000] print: 9[thread-0x70000cd57000] is waking upnow is: 2019-10-13 10:17:51

一次調(diào)用


  • 主要API

在一些情況下,我們有些任務(wù)需要執(zhí)行一次,并且我們只希望它執(zhí)行一次,例如資源的初始化任務(wù)。這個(gè)時(shí)候就可以用到上面的接口。這個(gè)接口會(huì)保證,即便在多線程的環(huán)境下,相應(yīng)的函數(shù)也只會(huì)調(diào)用一次。

下面就是一個(gè)示例:有三個(gè)線程都會(huì)使用init函數(shù),但是只會(huì)有一個(gè)線程真正執(zhí)行它。

// 05_call_once.cpp
void init() { cout << "Initialing..." << endl; // Do something...}
void worker(once_flag* flag) { call_once(*flag, init);}
int main() { once_flag flag;
thread t1(worker, &flag); thread t2(worker, &flag); thread t3(worker, &flag);
t1.join(); t2.join(); t3.join();
return 0;}

我們無(wú)法確定具體是哪一個(gè)線程會(huì)執(zhí)行init。而事實(shí)上,我們也不關(guān)心,因?yàn)橹灰心硞€(gè)線程完成這個(gè)初始化工作就可以了。

請(qǐng)思考一下,為什么要在main函數(shù)中創(chuàng)建once_flag flag。如果是在worker函數(shù)中直接聲明一個(gè)once_flag并使用行不行?為什么?


并發(fā)任務(wù)


下面以一個(gè)并發(fā)任務(wù)為示例講解如何引入多線程。

任務(wù)示例:現(xiàn)在假設(shè)我們需要計(jì)算某個(gè)范圍內(nèi)所有自然數(shù)的平方根之和,例如[1, 10e8]。

在單線程模型下,我們的代碼可能是這樣的:

// 06_naive_multithread.cpp
static const int MAX = 10e8; // ①static double sum = 0; // ②
void worker(int min, int max) { // ③ for (int i = min; i <= max; i++) { sum += sqrt(i); }}
void serial_task(int min, int max) { // ④ auto start_time = chrono::steady_clock::now(); sum = 0; worker(0, MAX); auto end_time = chrono::steady_clock::now(); auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count(); cout << "Serail task finish, " << ms << " ms consumed, Result: " << sum << endl;}

這段代碼說(shuō)明如下:

  1. 通過(guò)一個(gè)常量指定數(shù)據(jù)范圍,這個(gè)是為了方便調(diào)整。
  2. 通過(guò)一個(gè)全局變量來(lái)存儲(chǔ)結(jié)果。
  3. 通過(guò)一個(gè)任務(wù)函數(shù)來(lái)計(jì)算值。
  4. 統(tǒng)計(jì)任務(wù)的執(zhí)行時(shí)間。

這段程序輸出如下:

Serail task finish, 6406 ms consumed, Result: 2.10819e+13

很顯然,上面單線程的做法性能太差了。我們的任務(wù)完全是可以并發(fā)執(zhí)行的。并且任務(wù)很容易劃分。

下面我們就嘗試以多線程的方式來(lái)改造原先的程序。

改造后的程序如下:

// 06_naive_multithread.cpp
void concurrent_task(int min, int max) { auto start_time = chrono::steady_clock::now();
unsigned concurrent_count = thread::hardware_concurrency(); // ① cout << "hardware_concurrency: " << concurrent_count << endl; vector<thread> threads; min = 0; sum = 0; for (int t = 0; t < concurrent_count; t++) { // ② int range = max / concurrent_count * (t + 1); threads.push_back(thread(worker, min, range)); // ③ min = range + 1; } for (auto& t : threads) { t.join(); // ④ }
auto end_time = chrono::steady_clock::now(); auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count(); cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;}

這段代碼說(shuō)明如下:

  1. thread::hardware_concurrency()可以獲取到當(dāng)前硬件支持多少個(gè)線程并行執(zhí)行。
  2. 根據(jù)處理器的情況決定線程的數(shù)量。
  3. 對(duì)于每一個(gè)線程都通過(guò)worker函數(shù)來(lái)完成任務(wù),并劃分一部分?jǐn)?shù)據(jù)給它處理。
  4. 等待每一個(gè)線程執(zhí)行結(jié)束。

很好,似乎很簡(jiǎn)單就完成了并發(fā)的改造。然后我們運(yùn)行一下這個(gè)程序:

hardware_concurrency: 16Concurrent task finish, 6246 ms consumed, Result: 1.78162e+12

很抱歉,我們會(huì)發(fā)現(xiàn)這里的性能并沒(méi)有明顯的提升。更嚴(yán)重的是,這里的結(jié)果是錯(cuò)誤的。

要搞清楚為什么結(jié)果不正確我們需要更多的背景知識(shí)。

我們知道,對(duì)于現(xiàn)代的處理器來(lái)說(shuō),為了加速處理的速度,每個(gè)處理器都會(huì)有自己的高速緩存(Cache),這個(gè)高速緩存是與每個(gè)處理器相對(duì)應(yīng)的,如下圖所示:

事實(shí)上,目前大部分CPU的緩存已經(jīng)不只一層。


處理器在進(jìn)行計(jì)算的時(shí)候,高速緩存會(huì)參與其中,例如數(shù)據(jù)的讀和寫(xiě)。而高速緩存和系統(tǒng)主存(Memory)是有可能存在不一致的。即:某個(gè)結(jié)果計(jì)算后保存在處理器的高速緩存中了,但是沒(méi)有同步到主存中,此時(shí)這個(gè)值對(duì)于其他處理器就是不可見(jiàn)的。

事情還遠(yuǎn)不止這么簡(jiǎn)單。我們對(duì)于全局變量值的修改:sum += sqrt(i);這條語(yǔ)句,它并非是原子的。它其實(shí)是很多條指令的組合才能完成。假設(shè)在某個(gè)設(shè)備上,這條語(yǔ)句通過(guò)下面這幾個(gè)步驟來(lái)完成。它們的時(shí)序可能如下所示:


在時(shí)間點(diǎn)a的時(shí)候,所有線程對(duì)于sum變量的值是一致的。

但是在時(shí)間點(diǎn)b之后,thread3上已經(jīng)對(duì)sum進(jìn)行了賦值。而這個(gè)時(shí)候其他幾個(gè)線程也同時(shí)在其他處理器上使用了這個(gè)值,那么這個(gè)時(shí)候它們所使用的值就是舊的(錯(cuò)誤的)。最后得到的結(jié)果也自然是錯(cuò)的。

競(jìng)爭(zhēng)條件與臨界區(qū)


當(dāng)多個(gè)進(jìn)程或者線程同時(shí)訪問(wèn)共享數(shù)據(jù)時(shí),只要有一個(gè)任務(wù)會(huì)修改數(shù)據(jù),那么就可能會(huì)發(fā)生問(wèn)題。此時(shí)結(jié)果依賴(lài)于這些任務(wù)執(zhí)行的相對(duì)時(shí)間,這種場(chǎng)景稱(chēng)為競(jìng)爭(zhēng)條件(race condition)。

訪問(wèn)共享數(shù)據(jù)的代碼片段稱(chēng)之為臨界區(qū)(critical section)。具體到上面這個(gè)示例,臨界區(qū)就是讀寫(xiě)sum變量的地方。

要避免競(jìng)爭(zhēng)條件,就需要對(duì)臨界區(qū)進(jìn)行數(shù)據(jù)保護(hù)。

很自然的,現(xiàn)在我們能夠理解發(fā)生競(jìng)爭(zhēng)條件是因?yàn)檫@些線程在同時(shí)訪問(wèn)共享數(shù)據(jù),其中有些線程的改動(dòng)沒(méi)有讓其他線程知道,導(dǎo)致其他線程在錯(cuò)誤的基礎(chǔ)上進(jìn)行處理,結(jié)果自然也就是錯(cuò)誤的。

那么,如果一次只讓一個(gè)線程訪問(wèn)共享數(shù)據(jù),訪問(wèn)完了再讓其他線程接著訪問(wèn),這樣就可以避免問(wèn)題的發(fā)生了。

接下來(lái)介紹的API提供的就是這樣的功能。

互斥體與鎖


mutex


開(kāi)發(fā)并發(fā)系統(tǒng)的目的主要是為了提升性能:將任務(wù)分散到多個(gè)線程,然后在不同的處理器上同時(shí)執(zhí)行。這些分散開(kāi)來(lái)的線程通常會(huì)包含兩類(lèi)任務(wù):

  1. 獨(dú)立的對(duì)于劃分給自己的數(shù)據(jù)的處理
  2. 對(duì)于處理結(jié)果的匯總

其中第1項(xiàng)任務(wù)因?yàn)槊總€(gè)線程是獨(dú)立的,不存在競(jìng)爭(zhēng)條件的問(wèn)題。而第2項(xiàng)任務(wù),由于所有線程都可能往總結(jié)果(例如上面的sum變量)匯總,這就需要做保護(hù)了。在某一個(gè)具體的時(shí)刻,只應(yīng)當(dāng)有一個(gè)線程更新總結(jié)果,即:保證每個(gè)線程對(duì)于共享數(shù)據(jù)的訪問(wèn)是“互斥”的。mutex 就提供了這樣的功能。

mutex是mutual exclusion(互斥)的簡(jiǎn)寫(xiě)。

  • 主要API

很明顯,在這些類(lèi)中,mutex是最基礎(chǔ)的API。其他類(lèi)都是在它的基礎(chǔ)上的改進(jìn)。所以這些類(lèi)都提供了下面三個(gè)方法,并且它們的功能是一樣的:

| 方法| 說(shuō)明 |
| lock|鎖定互斥體,如果不可用,則阻塞 |
| try_lock |嘗試鎖定互斥體,如果不可用,直接返回 |
|unlock | 解鎖互斥體|

這三個(gè)方法提供了基礎(chǔ)的鎖定和解除鎖定的功能。使用lock意味著你有很強(qiáng)的意愿一定要獲取到互斥體,而使用try_lock則是進(jìn)行一次嘗試。這意味著如果失敗了,你通常還有其他的路徑可以走。

在這些基礎(chǔ)功能之上,其他的類(lèi)分別在下面三個(gè)方面進(jìn)行了擴(kuò)展:

  • 超時(shí):
    timed_mutex,
    recursive_timed_mutex,
    shared_timed_mutex
    名稱(chēng)都帶有timed,這意味著它們都支持超時(shí)功能。它們都提供了try_lock_for和try_lock_until方法,這兩個(gè)方法分別可以指定超時(shí)的時(shí)間長(zhǎng)度和時(shí)間點(diǎn)。如果在超時(shí)的時(shí)間范圍內(nèi)沒(méi)有能獲取到鎖,則直接返回,不再繼續(xù)等待。
  • 可重入:
    recursive_mutex和recursive_timed_mutex的名稱(chēng)都帶有recursive。可重入或者叫做可遞歸,是指在同一個(gè)線程中,同一把鎖可以鎖定多次。這就避免了一些不必要的死鎖。
  • 共享:
    shared_timed_mutex和shared_mutex提供了共享功能。對(duì)于這類(lèi)互斥體,實(shí)際上是提供了兩把鎖:一把是共享鎖,一把是互斥鎖。一旦某個(gè)線程獲取了互斥鎖,任何其他線程都無(wú)法再獲取互斥鎖和共享鎖;但是如果有某個(gè)線程獲取到了共享鎖,其他線程無(wú)法再獲取到互斥鎖,但是還有獲取到共享鎖。這里互斥鎖的使用和其他的互斥體接口和功能一樣。而共享鎖可以同時(shí)被多個(gè)線程同時(shí)獲取到(使用共享鎖的接口見(jiàn)下面的表格)。共享鎖通常用在讀者寫(xiě)者模型上。

使用共享鎖的接口如下:

| 方法| 說(shuō)明 |
|lock_shared | 獲取互斥體的共享鎖,如果無(wú)法獲取則阻塞 |
| try_lock_shared| 嘗試獲取共享鎖,如果不可用,直接返回 |
| unlock_shared| 解鎖共享鎖 |

接下來(lái),我們就借助剛學(xué)到的mutex來(lái)改造我們的并發(fā)系統(tǒng),改造后的程序如下:

// 07_mutex_lock.cpp
static const int MAX = 10e8;static double sum = 0;
static mutex exclusive;
void concurrent_worker(int min, int max) { for (int i = min; i <= max; i++) { exclusive.lock(); // ① sum += sqrt(i); exclusive.unlock(); // ② }}
void concurrent_task(int min, int max) { auto start_time = chrono::steady_clock::now();
unsigned concurrent_count = thread::hardware_concurrency(); cout << "hardware_concurrency: " << concurrent_count << endl; vector<thread> threads; min = 0; sum = 0; for (int t = 0; t < concurrent_count; t++) { int range = max / concurrent_count * (t + 1); threads.push_back(thread(concurrent_worker, min, range)); // ③ min = range + 1; } for (int i = 0; i < threads.size(); i++) { threads[i].join(); }
auto end_time = chrono::steady_clock::now(); auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count(); cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;}

這里只有三個(gè)地方需要關(guān)注:

  1. 在訪問(wèn)共享數(shù)據(jù)之前加鎖
  2. 訪問(wèn)完成之后解鎖
  3. 在多線程中使用帶鎖的版本

執(zhí)行之后結(jié)果輸出如下:

hardware_concurrency: 16Concurrent task finish, 74232 ms consumed, Result: 2.10819e+13

這下結(jié)果是對(duì)了,但是我們卻發(fā)現(xiàn)這個(gè)版本比原先單線程的版本性能還要差很多。這是為什么?

這是因?yàn)榧渔i和解鎖是有代價(jià)的,這里計(jì)算最耗時(shí)的地方在鎖里面,每次只能有一個(gè)線程串行執(zhí)行,相比于單線程模型,它不但是串行的,還增加了鎖的負(fù)擔(dān),因此就更慢了。

這就是為什么前面說(shuō)多線程系統(tǒng)會(huì)增加系統(tǒng)的復(fù)雜度,而且并非多線程系統(tǒng)一定就有更好的性能。

不過(guò),對(duì)于這里的問(wèn)題是可以改進(jìn)的。我們仔細(xì)思考一下:我們劃分給每個(gè)線程的數(shù)據(jù)其實(shí)是獨(dú)立的,對(duì)于數(shù)據(jù)的處理是耗時(shí)的,但其實(shí)這部分邏輯每個(gè)線程可以單獨(dú)處理,沒(méi)必要加鎖。只有在最后匯總數(shù)據(jù)的時(shí)候進(jìn)行一次鎖保護(hù)就可以了。

于是我們改造concurrent_worker,像下面這樣:

// 08_improved_mutex_lock.cpp
void concurrent_worker(int min, int max) { double tmp_sum = 0; for (int i = min; i <= max; i++) { tmp_sum += sqrt(i); // ① } exclusive.lock(); // ② sum += tmp_sum; exclusive.unlock();}

這段代碼的改變?cè)谟趦商帲?/span>

  1. 通過(guò)一個(gè)局部變量保存當(dāng)前線程的處理結(jié)果
  2. 在匯總總結(jié)過(guò)的時(shí)候進(jìn)行鎖保護(hù)

運(yùn)行一下改進(jìn)后的程序,其結(jié)果輸出如下:

hardware_concurrency: 16Concurrent task finish, 451 ms consumed, Result: 2.10819e+13

可以看到,性能一下就提升了好多倍。我們終于體驗(yàn)到多線程帶來(lái)的好處了。

我們用鎖的粒度(granularity)來(lái)描述鎖的范圍。細(xì)粒度(fine-grained)是指鎖保護(hù)較小的范圍,粗粒度(coarse-grained)是指鎖保護(hù)較大的范圍。出于性能的考慮,我們應(yīng)該保證鎖的粒度盡可能的細(xì)。并且,不應(yīng)該在獲取鎖的范圍內(nèi)執(zhí)行耗時(shí)的操作,例如執(zhí)行IO。如果是耗時(shí)的運(yùn)算,也應(yīng)該盡可能的移到鎖的外面。

In general, a lock should be held for only the minimum possible time needed to perform the required operations.
--《C++ Concurrency in Action》


死鎖


死鎖是并發(fā)系統(tǒng)很常見(jiàn)的一類(lèi)問(wèn)題。

死鎖是指:兩個(gè)或以上的運(yùn)算單元,每一方都在等待其他方釋放資源,但是所有方都不愿意釋放資源。結(jié)果是沒(méi)有任何一方能繼續(xù)推進(jìn)下去,于是整個(gè)系統(tǒng)無(wú)法再繼續(xù)運(yùn)轉(zhuǎn)。

死鎖在現(xiàn)實(shí)中也很常見(jiàn),例如:兩個(gè)孩子分別拿著玩具的一半然后哭著要從對(duì)方手里得到另外一半玩具,但是誰(shuí)都不肯讓步。

在成年人的世界里也會(huì)發(fā)生類(lèi)似的情況,例如下面這個(gè)交通狀況:


下面我們來(lái)看一個(gè)編程示例。

現(xiàn)在假設(shè)我們?cè)陂_(kāi)發(fā)一個(gè)銀行的系統(tǒng),這個(gè)系統(tǒng)包含了轉(zhuǎn)賬的功能。

首先我們創(chuàng)建一個(gè)Account類(lèi)來(lái)描述銀行賬號(hào)。由于這僅僅是一個(gè)演示使用的代碼,所以我們希望代碼足夠的簡(jiǎn)單。Account類(lèi)僅僅包含名稱(chēng)和金額兩個(gè)字段。

另外,為了支持并發(fā),這個(gè)類(lèi)包含了一個(gè)mutex對(duì)象,用來(lái)保護(hù)賬號(hào)金額,在讀寫(xiě)賬號(hào)金額時(shí)需要先加鎖保護(hù)。

// 09_deadlock_bank_transfer.cpp
class Account {public: Account(string name, double money): mName(name), mMoney(money) {};
public: void changeMoney(double amount) { mMoney += amount; } string getName() { return mName; } double getMoney() { return mMoney; } mutex* getLock() { return &mMoneyLock; }
private: string mName; double mMoney; mutex mMoneyLock;};

Account類(lèi)很簡(jiǎn)單,我想就不用多做說(shuō)明了。

接下來(lái),我們?cè)賱?chuàng)建一個(gè)描述銀行的Bank類(lèi)。

// 09_deadlock_bank_transfer.cpp
class Bank {public: void addAccount(Account* account) { mAccounts.insert(account); }
bool transferMoney(Account* accountA, Account* accountB, double amount) { lock_guard guardA(*accountA->getLock()); // ① lock_guard guardB(*accountB->getLock());
if (amount > accountA->getMoney()) { // ② return false; }
accountA->changeMoney(-amount); // ③ accountB->changeMoney(amount); return true; }
double totalMoney() const { double sum = 0; for (auto a : mAccounts) { sum += a->getMoney(); } return sum; }
private: set<Account*> mAccounts;};

銀行類(lèi)中記錄了所有的賬號(hào),并且提供了一個(gè)方法用來(lái)查詢(xún)整個(gè)銀行的總金額。
這其中,我們最主要要關(guān)注轉(zhuǎn)賬的實(shí)現(xiàn):transferMoney。該方法的幾個(gè)關(guān)鍵點(diǎn)如下:

  1. 為了保證線程安全,在修改每個(gè)賬號(hào)之前,需要獲取相應(yīng)的鎖。
  2. 判斷轉(zhuǎn)出賬戶(hù)金額是否足夠,如果不夠此次轉(zhuǎn)賬失敗。
  3. 進(jìn)行轉(zhuǎn)賬。

有了銀行和賬戶(hù)結(jié)構(gòu)之后就可以開(kāi)發(fā)轉(zhuǎn)賬系統(tǒng)了,同樣的,由于是為了演示所用,我們的轉(zhuǎn)賬系統(tǒng)也會(huì)盡可能的簡(jiǎn)單:

// 09_deadlock_bank_transfer.cpp
void randomTransfer(Bank* bank, Account* accountA, Account* accountB) { while(true) { double randomMoney = ((double)rand() / RAND_MAX) * 100; if (bank->transferMoney(accountA, accountB, randomMoney)) { cout << "Transfer " << randomMoney << " from " << accountA->getName() << " to " << accountB->getName() << ", Bank totalMoney: " << bank->totalMoney() << endl; } else { cout << "Transfer failed, " << accountA->getName() << " has only $" << accountA->getMoney() << ", but " << randomMoney << " required" << endl; } }}

這里每次生成一個(gè)隨機(jī)數(shù),然后通過(guò)銀行進(jìn)行轉(zhuǎn)賬。

最后我們?cè)趍ain函數(shù)中創(chuàng)建兩個(gè)線程,互相在兩個(gè)賬號(hào)之間來(lái)回轉(zhuǎn)賬:

// 09_deadlock_bank_transfer.cpp
int main() { Account a("Paul", 100); Account b("Moira", 100);
Bank aBank; aBank.addAccount(&a); aBank.addAccount(&b);
thread t1(randomTransfer, &aBank, &a, &b); thread t2(randomTransfer, &aBank, &b, &a);
t1.join(); t2.join();
return 0;}

至此,我們的銀行轉(zhuǎn)賬系統(tǒng)就開(kāi)發(fā)完成了。然后編譯并運(yùn)行,其結(jié)果可能像下面這樣:

...Transfer 13.2901 from Paul to Moira, Bank totalMoney: 20042.6259 from Moira to Paul, Bank totalMoney: 200Transfer failed, Moira has only $34.7581, but 66.3208 requiredTransfer failed, Moira has only $34.7581, but Transfer 93.191 from 53.9176 requiredTransfer 60.6146 from Moira to Paul, Bank totalMoney: 200Transfer 49.7304 from Moira to Paul, Bank totalMoney: 200Paul to Moira, Bank totalMoney: Transfer failed, Moira has only $17.6041, but 18.1186 requiredTransfer failed, Moira has only $17.6041, but 18.893 requiredTransfer failed, Moira has only $17.6041, but 34.7078 requiredTransfer failed, Moira has only $17.6041, but 33.9569 requiredTransfer 12.7899 from 200Moira to Paul, Bank totalMoney: 200Transfer failed, Moira has only $63.9373, but 80.9038 requiredTransfer 50.933 from Moira to Paul, Bank totalMoney: 200Transfer failed, Moira has only $13.0043, but 30.2056 requiredTransfer failed, Moira has only $Transfer 59.123 from Paul to Moira, Bank totalMoney: 200Transfer 29.0486 from Paul to Moira, Bank totalMoney: 20013.0043, but 64.7307 required

如果你運(yùn)行了這個(gè)程序,你會(huì)發(fā)現(xiàn)很快它就卡住不動(dòng)了。為什么?

因?yàn)榘l(fā)生了死鎖。

我們仔細(xì)思考一下這兩個(gè)線程的邏輯:這兩個(gè)線程可能會(huì)同時(shí)獲取其中一個(gè)賬號(hào)的鎖,然后又想獲取另外一個(gè)賬號(hào)的鎖,此時(shí)就發(fā)生了死鎖。如下圖所示:


當(dāng)然,發(fā)生死鎖的原因遠(yuǎn)不止上面這一種情況。如果兩個(gè)線程互相join就可能發(fā)生死鎖。還有在一個(gè)線程中對(duì)一個(gè)不可重入的互斥體(例如mutex而非recursive_mutex)多次加鎖也會(huì)死鎖。

你可能會(huì)覺(jué)得,我可不會(huì)這么傻,寫(xiě)出這樣的代碼。但實(shí)際上,很多時(shí)候是由于代碼的深層次嵌套導(dǎo)致了死鎖的發(fā)生,由于調(diào)用關(guān)系的復(fù)雜導(dǎo)致發(fā)現(xiàn)這類(lèi)問(wèn)題并不容易。

如果仔細(xì)看一下上面的輸出,我們會(huì)發(fā)現(xiàn)還有另外一個(gè)問(wèn)題:這里的輸出是亂的。兩個(gè)線程的輸出混雜在一起了。究其原因也很容易理解:兩個(gè)線程可能會(huì)同時(shí)輸出,沒(méi)有做好隔離。

下面我們就來(lái)逐步解決上面的問(wèn)題。

對(duì)于輸出混亂的問(wèn)題很好解決,專(zhuān)門(mén)用一把鎖來(lái)保護(hù)輸出邏輯即可:

// 10_improved_bank_transfer.cpp
mutex sCoutLock;void randomTransfer(Bank* bank, Account* accountA, Account* accountB) { while(true) { double randomMoney = ((double)rand() / RAND_MAX) * 100; if (bank->transferMoney(accountA, accountB, randomMoney)) { sCoutLock.lock(); cout << "Transfer " << randomMoney << " from " << accountA->getName() << " to " << accountB->getName() << ", Bank totalMoney: " << bank->totalMoney() << endl; sCoutLock.unlock(); } else { sCoutLock.lock(); cout << "Transfer failed, " << accountA->getName() << " has only " << accountA->getMoney() << ", but " << randomMoney << " required" << endl; sCoutLock.unlock(); } }}

請(qǐng)思考一下兩處lock和unlock調(diào)用,并考慮為什么不在while(true)下面寫(xiě)一次整體的加鎖和解鎖。


通用鎖定算法


  • 主要API
要避免死鎖,需要仔細(xì)的思考和設(shè)計(jì)業(yè)務(wù)邏輯。

有一個(gè)比較簡(jiǎn)單的原則可以避免死鎖,即:對(duì)所有的鎖進(jìn)行排序,每次一定要按照順序來(lái)獲取鎖,不允許亂序。例如:要獲取某個(gè)玩具,一定要先拿到鎖A,再拿到鎖B,才能玩玩具。這樣就不會(huì)死鎖了。

這個(gè)原則雖然簡(jiǎn)單,但卻不容易遵守。因?yàn)閿?shù)據(jù)常常是分散在很多地方的。

不過(guò)好消息是,C++ 11標(biāo)準(zhǔn)中為我們提供了一些工具來(lái)避免因?yàn)槎喟焰i而導(dǎo)致的死鎖。我們只要直接調(diào)用這些接口就可以了。這個(gè)就是上面提到的兩個(gè)函數(shù)。它們都支持傳入多個(gè)Lockable對(duì)象。

接下來(lái)我們用它來(lái)改造之前死鎖的轉(zhuǎn)賬系統(tǒng):

// 10_improved_bank_transfer.cpp
bool transferMoney(Account* accountA, Account* accountB, double amount) { lock(*accountA->getLock(), *accountB->getLock()); // ① lock_guard lockA(*accountA->getLock(), adopt_lock); // ② lock_guard lockB(*accountB->getLock(), adopt_lock); // ③
if (amount > accountA->getMoney()) { return false; }
accountA->changeMoney(-amount); accountB->changeMoney(amount); return true;}

這里只改動(dòng)了3行代碼。

  1. 這里通過(guò)lock函數(shù)來(lái)獲取兩把鎖,標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)會(huì)保證不會(huì)發(fā)生死鎖。
  2. lock_guard在下面我們還會(huì)詳細(xì)介紹。這里只要知道它會(huì)在自身對(duì)象生命周期的范圍內(nèi)鎖定互斥體即可。創(chuàng)建lock_guard的目的是為了在transferMoney結(jié)束的時(shí)候釋放鎖,lockB也是一樣。但需要注意的是,這里傳遞了 adopt_lock表示:現(xiàn)在是已經(jīng)獲取到互斥體了的狀態(tài)了,不用再次加鎖(如果不加adopt_lock就是二次鎖定了)。

運(yùn)行一下這個(gè)改造后的程序,其輸出如下所示:

...Transfer failed, Paul has only $1.76243, but 17.5974 requiredTransfer failed, Paul has only $1.76243, but 59.2104 requiredTransfer failed, Paul has only $1.76243, but 49.6379 requiredTransfer failed, Paul has only $1.76243, but 63.6373 requiredTransfer failed, Paul has only $1.76243, but 51.8742 requiredTransfer failed, Paul has only $1.76243, but 50.0081 requiredTransfer failed, Paul has only $1.76243, but 86.1041 requiredTransfer failed, Paul has only $1.76243, but 51.3278 requiredTransfer failed, Paul has only $1.76243, but 66.5754 requiredTransfer failed, Paul has only $1.76243, but 32.1867 requiredTransfer failed, Paul has only $1.76243, but 62.0039 requiredTransfer failed, Paul has only $1.76243, but 98.7819 requiredTransfer failed, Paul has only $1.76243, but 27.046 requiredTransfer failed, Paul has only $1.76243, but 62.9155 requiredTransfer 98.8478 from Moira to Paul, Bank totalMoney: 200Transfer 80.0722 from Moira to Paul, Bank totalMoney: 200Transfer 73.7035 from Moira to Paul, Bank totalMoney: 200Transfer 34.4476 from Moira to Paul, Bank totalMoney: 200Transfer failed, Moira has only $10.0142, but 61.3033 requiredTransfer failed, Moira has only $10.0142, but 24.5595 required...

現(xiàn)在這個(gè)轉(zhuǎn)賬程序會(huì)一直運(yùn)行下去,不會(huì)再死鎖了。輸出也是正常的了。

通用互斥管理


  • 主要API

互斥體(mutex相關(guān)類(lèi))提供了對(duì)于資源的保護(hù)功能,但是手動(dòng)的鎖定(調(diào)用lock或者try_lock)和解鎖(調(diào)用unlock)互斥體是要耗費(fèi)比較大的精力的,我們需要精心考慮和設(shè)計(jì)代碼才行。因?yàn)槲覀冃枰WC,在任何情況下,解鎖要和加鎖配對(duì),因?yàn)榧僭O(shè)出現(xiàn)一條路徑導(dǎo)致獲取鎖之后沒(méi)有正常釋放,就會(huì)影響整個(gè)系統(tǒng)。如果考慮方法還可以會(huì)拋出異常,這樣的代碼寫(xiě)起來(lái)會(huì)很費(fèi)勁。

鑒于這個(gè)原因,標(biāo)準(zhǔn)庫(kù)就提供了上面的這些API。它們都使用了叫做RAII的編程技巧,來(lái)簡(jiǎn)化我們手動(dòng)加鎖和解鎖的“體力活”。

請(qǐng)看下面的例子

// https://en.cppreference.com/w/cpp/thread/lock_guard
#include <thread>#include <mutex>#include <iostream> int g_i = 0;std::mutex g_i_mutex; // ① void safe_increment(){ std::lock_guard<std::mutex> lock(g_i_mutex); // ② ++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n'; // ③} int main(){ std::cout << "main: " << g_i << '\n'; std::thread t1(safe_increment); // ④ std::thread t2(safe_increment); t1.join(); t2.join(); std::cout << "main: " << g_i << '\n';}

這段代碼中:

  1. 全局的互斥體g_i_mutex用來(lái)保護(hù)全局變量g_i
  2. 這是一個(gè)設(shè)計(jì)為可以被多線程環(huán)境使用的方法。因此需要通過(guò)互斥體來(lái)進(jìn)行保護(hù)。這里沒(méi)有調(diào)用lock方法,而是直接使用lock_guard來(lái)鎖定互斥體。
  3. 在方法結(jié)束的時(shí)候,局部變量std::lock_guard<std::mutex> lock會(huì)被銷(xiāo)毀,它對(duì)互斥體的鎖定也就解除了。
  4. 在多個(gè)線程中使用這個(gè)方法。


RAII


上面的幾個(gè)類(lèi)(lock_guard,unique_lock,shared_lock,scoped_lock)都使用了一個(gè)叫做RAII的編程技巧。

RAII全稱(chēng)是Resource Acquisition Is Initialization,直譯過(guò)來(lái)就是:資源獲取即初始化。

RAII是一種C++編程技術(shù),它將必須在使用前請(qǐng)求的資源(例如:分配的堆內(nèi)存、執(zhí)行線程、打開(kāi)的套接字、打開(kāi)的文件、鎖定的互斥體、磁盤(pán)空間、數(shù)據(jù)庫(kù)連接等——任何存在受限供給中的事物)的生命周期與一個(gè)對(duì)象的生存周期相綁定。

RAII保證資源可用于任何會(huì)訪問(wèn)該對(duì)象的函數(shù)。它亦保證所有資源在其控制對(duì)象的生存期結(jié)束時(shí),以獲取順序的逆序釋放。類(lèi)似地,若資源獲取失敗(構(gòu)造函數(shù)以異常退出),則為已構(gòu)造完成的對(duì)象和基類(lèi)子對(duì)象所獲取的所有資源,會(huì)以初始化順序的逆序釋放。這有效地利用了語(yǔ)言特性以消除內(nèi)存泄漏并保證異常安全。

RAII 可總結(jié)如下:

  • 將每個(gè)資源封裝入一個(gè)類(lèi),其中:
    • 構(gòu)造函數(shù)請(qǐng)求資源,并建立所有類(lèi)不變式,或在它無(wú)法完成時(shí)拋出異常,
    • 析構(gòu)函數(shù)釋放資源并決不拋出異常;
  • 始終經(jīng)由 RAII 類(lèi)的實(shí)例使用滿(mǎn)足要求的資源,該資源
    • 自身?yè)碛凶詣?dòng)存儲(chǔ)期或臨時(shí)生存期,或
    • 具有與自動(dòng)或臨時(shí)對(duì)象的生存期綁定的生存期

回想一下上文中的transferMoney方法中的三行代碼:

lock(*accountA->getLock(), *accountB->getLock());lock_guard lockA(*accountA->getLock(), adopt_lock);lock_guard lockB(*accountB->getLock(), adopt_lock);

如果使用unique_lock這三行代碼還有一種等價(jià)的寫(xiě)法:

unique_lock lockA(*accountA->getLock(), defer_lock);unique_lock lockB(*accountB->getLock(), defer_lock);lock(*accountA->getLock(), *accountB->getLock());

請(qǐng)注意這里lock方法的調(diào)用位置。這里先定義unique_lock指定了defer_lock,因此實(shí)際沒(méi)有鎖定互斥體,而是到第三行才進(jìn)行鎖定。

最后,借助scoped_lock,我們可以將三行代碼合成一行,這種寫(xiě)法也是等價(jià)的。

scoped_lock lockAll(*accountA->getLock(), *accountB->getLock());

scoped_lock會(huì)在其生命周期范圍內(nèi)鎖定互斥體,銷(xiāo)毀的時(shí)候解鎖。同時(shí),它可以鎖定多個(gè)互斥體,并且避免死鎖。

目前,只還有shared_lock我們沒(méi)有提到。它與其他幾個(gè)類(lèi)的區(qū)別在于:它是以共享的方式鎖定互斥體。

條件變量


| API | C++標(biāo)準(zhǔn) | 說(shuō)明 |
| condition_variable | C++ 11 | 提供與 std::unique_lock 關(guān)聯(lián)的條件變量 |
| condition_variable_any | C++ 11 |提供與任何鎖類(lèi)型關(guān)聯(lián)的條件變量 |
| notify_all_at_thread_exit |C++ 11 | 安排到在此線程完全結(jié)束時(shí)對(duì) notify_all 的調(diào)用 |
| cv_status | C++ 11 |列出條件變量上定時(shí)等待的可能結(jié)果 |

至此,我們還有一個(gè)地方可以改進(jìn)。那就是:轉(zhuǎn)賬金額不足的時(shí)候,程序直接返回了false。這很難說(shuō)是一個(gè)好的策略。因?yàn)椋幢汶m然當(dāng)前賬號(hào)金額不足以轉(zhuǎn)賬,但只要?jiǎng)e的賬號(hào)又轉(zhuǎn)賬進(jìn)來(lái)之后,當(dāng)前這個(gè)轉(zhuǎn)賬操作也許就可以繼續(xù)執(zhí)行了。

這在很多業(yè)務(wù)中是很常見(jiàn)的一個(gè)需求:每一次操作都要正確執(zhí)行,如果條件不滿(mǎn)足就停下來(lái)等待,直到條件滿(mǎn)足之后再繼續(xù)。而不是直接返回。

條件變量提供了一個(gè)可以讓多個(gè)線程間同步協(xié)作的功能。這對(duì)于生產(chǎn)者-消費(fèi)者模型很有意義。在這個(gè)模型下:

  • 生產(chǎn)者和消費(fèi)者共享一個(gè)工作區(qū)。這個(gè)區(qū)間的大小是有限的。
  • 生產(chǎn)者總是產(chǎn)生數(shù)據(jù)放入工作區(qū)中,當(dāng)工作區(qū)滿(mǎn)了。它就停下來(lái)等消費(fèi)者消費(fèi)一部分?jǐn)?shù)據(jù),然后繼續(xù)工作。
  • 消費(fèi)者總是從工作區(qū)中拿出數(shù)據(jù)使用。當(dāng)工作區(qū)中的數(shù)據(jù)全部被消費(fèi)空了之后,它也會(huì)停下來(lái)等待生產(chǎn)者往工作區(qū)中放入新的數(shù)據(jù)。

從上面可以看到,無(wú)論是生產(chǎn)者還是消費(fèi)者,當(dāng)它們工作的條件不滿(mǎn)足時(shí),它們并不是直接報(bào)錯(cuò)返回,而是停下來(lái)等待,直到條件滿(mǎn)足。

下面我們就借助于條件變量,再次改造之前的銀行轉(zhuǎn)賬系統(tǒng)。

這個(gè)改造主要在于賬號(hào)類(lèi)。我們重點(diǎn)是要調(diào)整changeMoney方法。

// 11_bank_transfer_wait_notify.cpp
class Account {public: Account(string name, double money): mName(name), mMoney(money) {};
public: void changeMoney(double amount) { unique_lock lock(mMoneyLock); // ② mConditionVar.wait(lock, [this, amount] { // ③ return mMoney + amount > 0; // ④ }); mMoney += amount; mConditionVar.notify_all(); // ⑤ }
string getName() { return mName; }
double getMoney() { return mMoney; }
private: string mName; double mMoney; mutex mMoneyLock; condition_variable mConditionVar; // ①};

這幾處改動(dòng)說(shuō)明如下:

  1. 這里聲明了一個(gè)條件變量,用來(lái)在多個(gè)線程之間協(xié)作。
  2. 這里使用的是unique_lock,這是為了與條件變量相配合。因?yàn)闂l件變量會(huì)解鎖和重新鎖定互斥體。
  3. 這里是比較重要的一個(gè)地方:通過(guò)條件變量進(jìn)行等待。此時(shí):會(huì)通過(guò)后面的lambda表達(dá)式判斷條件是否滿(mǎn)足。如果滿(mǎn)足則繼續(xù);如果不滿(mǎn)足,則此處會(huì)解鎖互斥體,并讓當(dāng)前線程等待。解鎖這一點(diǎn)非常重要,因?yàn)橹挥羞@樣,才能讓其他線程獲取互斥體。
  4. 這里是條件變量等待的條件。如果你不熟悉lambda表達(dá)式,請(qǐng)自行網(wǎng)上學(xué)習(xí),或者閱讀我之前寫(xiě)的文章。
  5. 此處也很重要。當(dāng)金額發(fā)生變動(dòng)之后,我們需要通知所有在條件變量上等待的其他線程。此時(shí)所有調(diào)用wait線程都會(huì)再次喚醒,然后嘗試獲取鎖(當(dāng)然,只有一個(gè)能獲取到)并再次判斷條件是否滿(mǎn)足。除了notify_all還有notify_one,它只通知一個(gè)等待的線程。wait和notify就構(gòu)成了線程間互相協(xié)作的工具。

請(qǐng)注意:wait和notify_all雖然是寫(xiě)在一個(gè)函數(shù)中的,但是在運(yùn)行時(shí)它們是在多線程環(huán)境中執(zhí)行的,因此對(duì)于這段代碼,需要能夠從不同線程的角度去思考代碼的邏輯。這也是開(kāi)發(fā)并發(fā)系統(tǒng)比較難的地方。

有了上面的改動(dòng)之后,銀行的轉(zhuǎn)賬方法實(shí)現(xiàn)起來(lái)就很簡(jiǎn)單了,不用再考慮數(shù)據(jù)保護(hù)的問(wèn)題了:

// 11_bank_transfer_wait_notify.cpp
void Bank::transferMoney(Account* accountA, Account* accountB, double amount) { accountA->changeMoney(-amount); accountB->changeMoney(amount);}

當(dāng)然,轉(zhuǎn)賬邏輯也會(huì)變得簡(jiǎn)單,不用再管轉(zhuǎn)賬失敗的情況發(fā)生。

// 11_bank_transfer_wait_notify.cpp
mutex sCoutLock;void randomTransfer(Bank* bank, Account* accountA, Account* accountB) { while(true) { double randomMoney = ((double)rand() / RAND_MAX) * 100; { lock_guard guard(sCoutLock); cout << "Try to Transfer " << randomMoney << " from " << accountA->getName() << "(" << accountA->getMoney() << ") to " << accountB->getName() << "(" << accountB->getMoney() << "), Bank totalMoney: " << bank->totalMoney() << endl; } bank->transferMoney(accountA, accountB, randomMoney); }}

修改完之后的程序運(yùn)行輸出如下:

...Try to Transfer 13.72 from Moira(10.9287) to Paul(189.071), Bank totalMoney: 200Try to Transfer 28.6579 from Paul(189.071) to Moira(10.9287), Bank totalMoney: 200Try to Transfer 91.8049 from Paul(160.413) to Moira(39.5866), Bank totalMoney: 200Try to Transfer 5.56383 from Paul(82.3285) to Moira(117.672), Bank totalMoney: 200Try to Transfer 11.3594 from Paul(76.7646) to Moira(123.235), Bank totalMoney: 200Try to Transfer 16.9557 from Paul(65.4053) to Moira(134.595), Bank totalMoney: 200Try to Transfer 74.998 from Paul(48.4495) to Moira(151.55), Bank totalMoney: 200Try to Transfer 65.3005 from Moira(151.55) to Paul(48.4495), Bank totalMoney: 200Try to Transfer 90.6084 from Moira(86.25) to Paul(113.75), Bank totalMoney: 125.002Try to Transfer 99.6425 from Moira(70.6395) to Paul(129.36), Bank totalMoney: 200Try to Transfer 55.2091 from Paul(129.36) to Moira(70.6395), Bank totalMoney: 200Try to Transfer 92.259 from Paul(74.1513) to Moira(125.849), Bank totalMoney: 200...
這下比之前都要好了。

但是細(xì)心的讀者會(huì)發(fā)現(xiàn),Bank totalMoney的輸出有時(shí)候是200,有時(shí)候不是。但不管怎樣,即便這一次不是,下一次又是了。關(guān)于這一點(diǎn),請(qǐng)讀者自行思考一下為什么,以及如何改進(jìn)。

future


這一小節(jié)中,我們來(lái)熟悉更多的可以在并發(fā)環(huán)境中使用的工具,它們都位于<future>頭文件中。

async


很多語(yǔ)言都提供了異步的機(jī)制。異步使得耗時(shí)的操作不影響當(dāng)前主線程的執(zhí)行流。

在C++11中,async便是完成這樣的功能的。下面是一個(gè)代碼示例:

// 12_async_task.cpp
static const int MAX = 10e8;static double sum = 0;
void worker(int min, int max) { for (int i = min; i <= max; i++) { sum += sqrt(i); }}
int main() { sum = 0; auto f1 = async(worker, 0, MAX); cout << "Async task triggered" << endl; f1.wait(); cout << "Async task finish, result: " << sum << endl << endl;}

這仍然是我們之前熟悉的例子。這里有兩個(gè)地方需要說(shuō)明:

  1. 這里以異步的方式啟動(dòng)了任務(wù)。它會(huì)返回一個(gè)future對(duì)象。future用來(lái)存儲(chǔ)異步任務(wù)的執(zhí)行結(jié)果,關(guān)于future我們?cè)诤竺鎝ackaged_task的例子中再詳細(xì)說(shuō)明。在這個(gè)例子中我們僅僅用它來(lái)等待任務(wù)執(zhí)行完成。
  2. 此處是等待異步任務(wù)執(zhí)行完成。

需要注意的是,默認(rèn)情況下,async是啟動(dòng)一個(gè)新的線程,還是以同步的方式(不啟動(dòng)新的線程)運(yùn)行任務(wù),這一點(diǎn)標(biāo)準(zhǔn)是沒(méi)有指定的,由具體的編譯器決定。如果希望一定要以新的線程來(lái)異步執(zhí)行任務(wù),可以通過(guò)launch::async來(lái)明確說(shuō)明。launch中有兩個(gè)常量:

  • async:運(yùn)行新線程,以異步執(zhí)行任務(wù)。
  • deferred:調(diào)用方線程上第一次請(qǐng)求其結(jié)果時(shí)才執(zhí)行任務(wù),即惰性求值。

除了通過(guò)函數(shù)來(lái)指定異步任務(wù),還可以lambda表達(dá)式的方式來(lái)指定。如下所示:

// 12_async_task.cpp
int main() {
double result = 0; cout << "Async task with lambda triggered, thread: " << this_thread::get_id() << endl; auto f2 = async(launch::async, [&result]() { cout << "Lambda task in thread: " << this_thread::get_id() << endl; for (int i = 0; i <= MAX; i++) { result += sqrt(i); } }); f2.wait(); cout << "Async task with lambda finish, result: " << result << endl << endl; return 0;}

在上面這段代碼中,我們使用一個(gè)lambda表達(dá)式來(lái)編寫(xiě)異步任務(wù)的邏輯,并通過(guò)launch::async明確指定要通過(guò)獨(dú)立的線程來(lái)執(zhí)行任務(wù),同時(shí)我們打印出了線程的id。

這段代碼輸出如下:

Async task with lambda triggered, thread: 0x11290d5c0Lambda task in thread: 0x700007aa1000Async task with lambda finish, result: 2.10819e+13


對(duì)于面向?qū)ο缶幊虂?lái)說(shuō),很多時(shí)候肯定希望以對(duì)象的方法來(lái)指定異步任務(wù)。下面是一個(gè)示例:

// 12_async_task.cpp
class Worker {public: Worker(int min, int max): mMin(min), mMax(max) {} // ① double work() { // ② mResult = 0; for (int i = mMin; i <= mMax; i++) { mResult += sqrt(i); } return mResult; } double getResult() { return mResult; }
private: int mMin; int mMax; double mResult;};
int main() { Worker w(0, MAX); cout << "Task in class triggered" << endl; auto f3 = async(&Worker::work, &w); // ③ f3.wait(); cout << "Task in class finish, result: " << w.getResult() << endl << endl;
return 0;}

這段代碼有三處需要說(shuō)明:

  1. 這里通過(guò)一個(gè)類(lèi)來(lái)描述任務(wù)。這個(gè)類(lèi)是對(duì)前面提到的任務(wù)的封裝。它包含了任務(wù)的輸入?yún)?shù),和輸出結(jié)果。
  2. work函數(shù)是任務(wù)的主體邏輯。
  3. 通過(guò)async執(zhí)行任務(wù):這里指定了具體的任務(wù)函數(shù)以及相應(yīng)的對(duì)象。請(qǐng)注意這里是&w,因此傳遞的是對(duì)象的指針。如果不寫(xiě)&將傳入w對(duì)象的臨時(shí)復(fù)制。


packaged_task


在一些業(yè)務(wù)中,我們可能會(huì)有很多的任務(wù)需要調(diào)度。這時(shí)我們常常會(huì)設(shè)計(jì)出任務(wù)隊(duì)列和線程池的結(jié)構(gòu)。此時(shí),就可以使用packaged_task來(lái)包裝任務(wù)。

如果你了解設(shè)計(jì)模式,你應(yīng)該會(huì)知道命令模式。

packaged_task綁定到一個(gè)函數(shù)或者可調(diào)用對(duì)象上。當(dāng)它被調(diào)用時(shí),它就會(huì)調(diào)用其綁定的函數(shù)或者可調(diào)用對(duì)象。并且,可以通過(guò)與之相關(guān)聯(lián)的future來(lái)獲取任務(wù)的結(jié)果。調(diào)度程序只需要處理packaged_task,而非各個(gè)函數(shù)。

packaged_task對(duì)象是一個(gè)可調(diào)用對(duì)象,它可以被封裝成一個(gè)std::fucntion,或者作為線程函數(shù)傳遞給std::thread,或者直接調(diào)用。

下面是一個(gè)代碼示例:

// 13_packaged_task.cpp
double concurrent_worker(int min, int max) { double sum = 0; for (int i = min; i <= max; i++) { sum += sqrt(i); } return sum;}
double concurrent_task(int min, int max) { vector<future<double>> results; // ①
unsigned concurrent_count = thread::hardware_concurrency(); min = 0; for (int i = 0; i < concurrent_count; i++) { // ② packaged_task<double(int, int)> task(concurrent_worker); // ③ results.push_back(task.get_future()); // ④
int range = max / concurrent_count * (i + 1); thread t(std::move(task), min, range); // ⑤ t.detach();
min = range + 1; }
cout << "threads create finish" << endl; double sum = 0; for (auto& r : results) { sum += r.get(); ⑥ } return sum;}
int main() { auto start_time = chrono::steady_clock::now();
double r = concurrent_task(0, MAX);
auto end_time = chrono::steady_clock::now(); auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count(); cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << r << endl; return 0;}

在這段代碼中:

  1. 首先創(chuàng)建一個(gè)集合來(lái)存儲(chǔ)future對(duì)象。我們將用它來(lái)獲取任務(wù)的結(jié)果。
  2. 同樣的,根據(jù)CPU的情況來(lái)創(chuàng)建線程的數(shù)量。
  3. 將任務(wù)包裝成packaged_task。請(qǐng)注意,由于concurrent_worker被包裝成了任務(wù),我們無(wú)法直接獲取它的return值。而是要通過(guò)future對(duì)象來(lái)獲取。
  4. 獲取任務(wù)關(guān)聯(lián)的future對(duì)象,并將其存入集合中。
  5. 通過(guò)一個(gè)新的線程來(lái)執(zhí)行任務(wù),并傳入需要的參數(shù)。
  6. 通過(guò)future集合,逐個(gè)獲取每個(gè)任務(wù)的計(jì)算結(jié)果,將其累加。這里r.get()獲取到的就是每個(gè)任務(wù)中concurrent_worker的返回值。

為了簡(jiǎn)單起見(jiàn),這里的示例只使用了我們熟悉的例子和結(jié)構(gòu)。但在實(shí)際上的工程中,調(diào)用關(guān)系通常更復(fù)雜,你可以借助于packaged_task將任務(wù)組裝成隊(duì)列,然后通過(guò)線程池的方式進(jìn)行調(diào)度:


promise與future


在上面的例子中,concurrent_task的結(jié)果是通過(guò)return返回的。但在一些時(shí)候,我們可能不能這么做:在得到任務(wù)結(jié)果之后,可能還有一些事情需要繼續(xù)處理,例如清理工作。

這個(gè)時(shí)候,就可以將promise與future配對(duì)使用。這樣就可以將返回結(jié)果和任務(wù)結(jié)束兩個(gè)事情分開(kāi)。

下面是對(duì)上面代碼示例的改寫(xiě):

// 14_promise_future.cpp
double concurrent_worker(int min, int max) { double sum = 0; for (int i = min; i <= max; i++) { sum += sqrt(i); } return sum;}
void concurrent_task(int min, int max, promise<double>* result) { // ① vector<future<double>> results;
unsigned concurrent_count = thread::hardware_concurrency(); min = 0; for (int i = 0; i < concurrent_count; i++) { packaged_task<double(int, int)> task(concurrent_worker); results.push_back(task.get_future());
int range = max / concurrent_count * (i + 1); thread t(std::move(task), min, range); t.detach();
min = range + 1; }
cout << "threads create finish" << endl; double sum = 0; for (auto& r : results) { sum += r.get(); } result->set_value(sum); // ② cout << "concurrent_task finish" << endl;}
int main() { auto start_time = chrono::steady_clock::now();
promise<double> sum; // ③ concurrent_task(0, MAX, &sum);
auto end_time = chrono::steady_clock::now(); auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count(); cout << "Concurrent task finish, " << ms << " ms consumed." << endl; cout << "Result: " << sum.get_future().get() << endl; // ④ return 0;}

這段代碼和上面的示例在很大程度上是一樣的。只有小部分內(nèi)容做了改動(dòng):

  1. concurrent_task不再直接返回計(jì)算結(jié)果,而是增加了一個(gè)promise對(duì)象來(lái)存放結(jié)果。
  2. 在任務(wù)計(jì)算完成之后,將總結(jié)過(guò)設(shè)置到promise對(duì)象上。一旦這里調(diào)用了set_value,其相關(guān)聯(lián)的future對(duì)象就會(huì)就緒。
  3. 這里是在main中創(chuàng)建一個(gè)promoise來(lái)存放結(jié)果,并以指針的形式傳遞進(jìn)concurrent_task中。
  4. 通過(guò)sum.get_future().get()來(lái)獲取結(jié)果。第2點(diǎn)中已經(jīng)說(shuō)了:一旦調(diào)用了set_value,其相關(guān)聯(lián)的future對(duì)象就會(huì)就緒。

需要注意的是,future對(duì)象只有被一個(gè)線程獲取值。并且在調(diào)用get()之后,就沒(méi)有可以獲取的值了。如果從多個(gè)線程調(diào)用get()會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng),其結(jié)果是未定義的。

如果真的需要在多個(gè)線程中獲取future的結(jié)果,可以使用shared_future。

并行算法

從C++17開(kāi)始。<algorithm>和<numeric> 頭文件的中的很多算法都添加了一個(gè)新的參數(shù):sequenced_policy。

借助這個(gè)參數(shù),開(kāi)發(fā)者可以直接使用這些算法的并行版本,不用再自己創(chuàng)建并發(fā)系統(tǒng)和劃分?jǐn)?shù)據(jù)來(lái)調(diào)度這些算法。

sequenced_policy可能的取值有三種,它們的說(shuō)明如下:



注意:本文的前面已經(jīng)提到,目前clang編譯器還不支持這個(gè)功能。因此想要編譯這部分代碼,你需要使用gcc 9.0或更高版本,同時(shí)還需要安裝Intel Threading Building Blocks。

下面還是通過(guò)一個(gè)示例來(lái)進(jìn)行說(shuō)明:

// 15_parallel_algorithm.cpp

void generateRandomData(vector<double>& collection, int size) { random_device rd; mt19937 mt(rd()); uniform_real_distribution<double> dist(1.0, 100.0); for (int i = 0; i < size; i++) { collection.push_back(dist(mt)); }}



int main() { vector<double> collection; generateRandomData(collection, 10e6); // ①

vector<double> copy1(collection); // ② vector<double> copy2(collection); vector<double> copy3(collection);

auto time1 = chrono::steady_clock::now(); // ③ sort(execution::seq, copy1.begin(), copy1.end()); // ④ auto time2 = chrono::steady_clock::now(); auto duration = chrono::duration_cast<chrono::milliseconds>(time2 - time1).count(); cout << "Sequenced sort consuming " << duration << "ms." << endl; // ⑤

auto time3 = chrono::steady_clock::now(); sort(execution::par, copy2.begin(),copy2.end()); // ⑥ auto time4 = chrono::steady_clock::now(); duration = chrono::duration_cast<chrono::milliseconds>(time4 - time3).count(); cout << "Parallel sort consuming " << duration << "ms." << endl;

auto time5 = chrono::steady_clock::now(); sort(execution::par_unseq, copy2.begin(),copy2.end()); // ⑦ auto time6 = chrono::steady_clock::now(); duration = chrono::duration_cast<chrono::milliseconds>(time6 - time5).count(); cout << "Parallel unsequenced sort consuming " << duration << "ms." << endl;}

這段代碼很簡(jiǎn)單:

  1. 通過(guò)一個(gè)函數(shù)生成1000,000個(gè)隨機(jī)數(shù)。

  2. 將數(shù)據(jù)拷貝3份,以備使用。

  3. 接下來(lái)將通過(guò)三個(gè)不同的parallel_policy參數(shù)來(lái)調(diào)用同樣的sort算法。每次調(diào)用記錄開(kāi)始和結(jié)束的時(shí)間。

  4. 第一次調(diào)用使用std::execution::seq參數(shù)。

  5. 輸出本次測(cè)試所使用的時(shí)間。

  6. 第二次調(diào)用使用std::execution::par參數(shù)。

  7. 第三次調(diào)用使用std::execution::par_unseq參數(shù)。


該程序的輸出如下:

Sequenced sort consuming 4464ms.Parallel sort consuming 459ms.Parallel unsequenced sort consuming 168ms.

可以看到,性能最好的和最差的相差了超過(guò)26倍。

結(jié)束語(yǔ)

在本篇文章中,我們介紹了C++語(yǔ)言中新增的并發(fā)編程API。雖然這部分內(nèi)容已經(jīng)不少(大部分人很難一次性搞懂所有這些內(nèi)容,包括我自己),但實(shí)際上還有一個(gè)很重要的話題我們沒(méi)有觸及,那就是“內(nèi)存模型”。

C++內(nèi)存模型是C++11標(biāo)準(zhǔn)中最重要的特性之一。它是多線程環(huán)境能夠可靠工作的基礎(chǔ)。考慮到這部分內(nèi)容還需要比較多的篇幅來(lái)說(shuō)明,因此我們會(huì)在下一篇文章中繼續(xù)討論。

C++學(xué)習(xí)資料免費(fèi)獲取方法:關(guān)注程序喵大人,后臺(tái)回復(fù)“程序喵”即可免費(fèi)獲取40萬(wàn)字C++進(jìn)階獨(dú)家學(xué)習(xí)資料。





往期推薦


1、少寫(xiě)點(diǎn)
if-else吧,它的效率有多低你知道嗎?
2、年度原創(chuàng)好文匯總
3、全網(wǎng)首發(fā)!!C++20新特性全在這一張圖里了
4
他來(lái)了,他來(lái)了,C+
+17新特性精華都在這了
5、一文讓你搞懂設(shè)計(jì)模式
6、C++11新特性,所有知識(shí)點(diǎn)都在這了!
亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
99国产精品视频免费观看| 免费在线成人av| 在线观看视频亚洲| 亚洲国产精品专区久久| 99国产麻豆精品| 午夜久久福利| 欧美国产日韩一区| 国产精品毛片一区二区三区 | 欧美午夜在线观看| 国产一区二区三区在线观看视频| 亚洲精品永久免费精品| 午夜在线观看免费一区| 欧美日韩国产在线看| 韩国欧美一区| 亚洲自拍偷拍麻豆| 欧美极品aⅴ影院| 伊人蜜桃色噜噜激情综合| 亚洲视频在线免费观看| 男女精品视频| 一区二区三区我不卡| 午夜精品免费在线| 欧美视频在线观看免费网址| 亚洲国产成人久久综合一区| 午夜久久电影网| 国产精品大片| 99伊人成综合| 欧美女人交a| 亚洲精品中文字幕女同| 嫩模写真一区二区三区三州| 国产性色一区二区| 午夜精品区一区二区三| 欧美视频三区在线播放| 亚洲精品久久久久久下一站| 美女性感视频久久久| 国产一区二区三区视频在线观看| 欧美激情精品久久久六区热门| 国产精品视频1区| 中文欧美日韩| 欧美乱人伦中文字幕在线| 在线观看日韩国产| 久久久水蜜桃| 黄色精品免费| 久久在线免费视频| 永久免费视频成人| 久久永久免费| 伊人夜夜躁av伊人久久| 久久精品国产成人| 海角社区69精品视频| 久久嫩草精品久久久久| 激情国产一区二区| 国产精品国产三级国产aⅴ9色 | 欧美好吊妞视频| 影音先锋亚洲精品| 久久久人人人| 黄色精品在线看| 噜噜噜在线观看免费视频日韩| 尤物精品国产第一福利三区 | 国产亚洲欧美日韩日本| 久久成人免费网| 影音先锋久久| 欧美精品一区二区三区视频| 一本久道综合久久精品| 国产精品久久久久影院色老大 | 国模吧视频一区| 午夜精品理论片| 国产日韩欧美在线| 久久久久这里只有精品| 亚洲缚视频在线观看| 欧美激情精品久久久久久久变态| 日韩午夜免费视频| 国产精品国产一区二区| 久久精品国产免费观看| 亚洲国产成人av好男人在线观看| 欧美第一黄网免费网站| 一区二区三区视频在线 | 亚洲黄色性网站| 在线电影院国产精品| 久久这里只有| 一区二区冒白浆视频| 国产视频一区在线| 欧美88av| 亚洲欧美国产77777| 在线观看日韩专区| 国产精品xxx在线观看www| 欧美日韩精品二区| 亚洲一区在线视频| 国产精品久久国产三级国电话系列 | 亚洲欧美日韩在线不卡| 国产婷婷色一区二区三区四区| 蜜桃久久av| 亚洲欧美在线观看| 亚洲人精品午夜| 国产日韩专区| 欧美激情第一页xxx| 欧美中文字幕精品| 9i看片成人免费高清| 一区二区在线观看视频| 国产精品久久久久久福利一牛影视 | 国产精品欧美激情| 久久综合网色—综合色88| 亚洲小视频在线观看| 亚洲国产视频直播| 国产亚洲综合在线| 欧美日韩亚洲一区二区三区在线观看 | 亚洲黄色成人久久久| 久久综合给合| 亚洲国产你懂的| 国产精品激情电影| 久热综合在线亚洲精品| 亚洲欧美网站| 一本色道久久综合狠狠躁的推荐| 黄色亚洲网站| 国产麻豆成人精品| 欧美日韩国产不卡| 猛男gaygay欧美视频| 欧美亚洲一区二区三区| 一本到高清视频免费精品| 亚洲大片av| 尤物yw午夜国产精品视频明星| 国产欧美日韩精品一区| 欧美日韩理论| 欧美精品一区二区视频| 久久欧美中文字幕| 久久精品99国产精品| 亚洲视频欧洲视频| 亚洲另类一区二区| 亚洲国产欧美另类丝袜| 伊人久久大香线| 国产一区二区三区免费观看| 国产精品久久久久久亚洲毛片| 欧美韩日视频| 欧美黄色免费| 你懂的国产精品| 欧美国产日韩一区| 欧美激情 亚洲a∨综合| 欧美高清视频在线 | 韩国av一区二区三区在线观看| 欧美日韩不卡视频| 欧美激情在线| 欧美看片网站| 欧美日韩免费观看中文| 欧美视频日韩| 国产精品一级二级三级| 国产老女人精品毛片久久| 国产精品入口夜色视频大尺度| 国产精品美女午夜av| 国产精品久久久久久久第一福利| 欧美色区777第一页| 国产精品videosex极品| 欧美视频日韩| 国产精品视频免费在线观看| 国产精品日韩一区二区| 国产欧美精品一区二区三区介绍 | 欧美精品97| 欧美日韩福利在线观看| 欧美亚洲成人免费| 国产美女一区| 又紧又大又爽精品一区二区| 亚洲高清在线播放| 亚洲精品免费观看| 亚洲永久视频| 久久国产高清| 免费久久99精品国产自在现线| 欧美a级片一区| 欧美午夜激情在线| 国产欧美午夜| 亚洲福利专区| 亚洲一二三区在线| 国产精品福利在线观看| 国产精品嫩草99av在线| 黄色国产精品| 日韩亚洲欧美高清| 亚洲欧美另类久久久精品2019| 欧美在线观看网址综合| 免费av成人在线| 国产精品欧美一区二区三区奶水| 黄色精品一区| 中文在线一区| 久久九九国产精品怡红院| 欧美精品一区二区三区一线天视频 | 久久久久一区二区三区| 久久久亚洲国产天美传媒修理工| 欧美成人精品三级在线观看 | 亚洲视频在线播放| 午夜精品网站| 欧美激情第三页| 国产日韩欧美a| 亚洲伦理在线免费看| 欧美一区二区视频在线观看| 欧美高清在线一区二区| 国产美女精品一区二区三区 | 久久女同精品一区二区| 欧美精品在线观看91| 国产一区二区欧美| 一区二区三区成人| 免费美女久久99| 国产欧美日韩精品专区| 99精品欧美| 久久青青草原一区二区| 国产精品国产亚洲精品看不卡15|