C++11 併發程式設計教程&學習筆記
C++11 併發程式設計教程&學習筆記
為了能夠編譯本文的示例程式碼,你需要有一個支援 C++11 的編譯器,筆者使用的是 TDM-GCC4.9.2
C++11 併發程式設計教程 - Part 1 : thread 初探
啟動執行緒
建立一個 std::thread 的例項時,便會自行啟動。
建立執行緒例項時,必須提供該執行緒將要執行的函式,方法之一是傳遞一個函式指標。
#include <thread>
#include <iostream>
using std::cout;
using std ::endl;
using std::thread;
void hello(){
cout << "Hello from thread" << endl;
}
int main(){
thread t1(hello);
t1.join(); // 直到t1結束後,main才返回,若不加該句,則可能t1未完成main就返回了
return 0;
}
所有的執行緒工具均置於標頭檔案 thread 中。
這個例子中值得注意的是對函式 join() 的呼叫。該呼叫將導致當前執行緒等待被 join 的執行緒結束(在本例中即執行緒 main 必須等待執行緒 t1 結束後方可繼續執行)。如果你忽略掉對 join() 的呼叫,其結果是未定義的 —— 程式可能打印出 “Hello from thread” 以及一個換行,或者只打印出 “Hello from thread” 卻沒有換行,甚至什麼都不做,那是因為執行緒main 可能線上程 t1 結束之前就返回了。
區分執行緒
每個執行緒都有唯一的 ID 以便我們加以區分。使用 std::thread 類的 get_id() 便可獲取標識對應執行緒的唯一 ID。
使用 std::this_thread 來獲取當前執行緒的引用。下面的例子將建立一些執行緒並使它們列印自己的 ID:
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
void hello(){
// std::this_thread 當前執行緒的引用
// get_id()獲取標識程序執行緒的唯一ID
cout << "Hello from thread" << this_thread::get_id() << endl;
}
int main(){
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(thread(hello));
}
for(auto& th : threads){
th.join();
}
return 0;
}
執行緒之間存在 interleaving ,某個執行緒可能隨時被搶佔。
又因為輸出到 ostream 分幾個步驟(首先輸出一個 string,然後是 ID,最後輸出換行),
因此一個執行緒可能執行了第一個步驟後就被其他執行緒搶佔了,直到其他所有執行緒列印完之後才能進行後面的步驟。
使用Lambda表示式啟動執行緒
當執行緒所要執行的程式碼非常短小時,沒有必要專門為之建立一個函式,可使用 Lambda表示式。
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(std::thread(
[](){
cout << "Hello from thread " << this_thread::get_id() << endl;
}));
}
for(auto& th : threads){
th.join();
}
return 0;
}
C++11 併發程式設計教程 - Part 2 : 保護共享資料
上面的程式碼中,執行緒互相獨立,通常情況下,執行緒之間可能用到共享資料。一旦對共享資料進行操作,就面臨著一個新的問題 —— 同步。
同步問題
我們就拿一個簡單的計數器作為示例吧。
這個計數器是一個結構體,他擁有一個計數變數,以及增加或減少計數的函式,看起來像這個樣子:
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
struct Counter {
int m_nValue;
Counter(int n = 0) : m_nValue(n){}
void increment(){ ++m_nValue; }
};
int main(){
Counter counter;
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(thread([&counter](){
for(int i = 0; i < 999999; ++i){
counter.increment();
}
}));
}
for(auto& th : threads){
th.join();
}
cout << counter.m_nValue << endl;
return 0;
}
每次執行結果會不同,因為計數器的increment()操作並非原子操作,而是由3個獨立的操作組成的:
1、讀取m_nValue變數的當前值。
2、將讀取的當前值+1
3、將+1後的值寫回value變數。
當單執行緒執行上述程式碼,每次執行的結果是一樣的,上述三個步驟會按順序進行。但是在多程序情況下,可能存在如下執行順序:
執行緒a:讀取 m_nValue的當前值,得到值為 0。加1。得到1,但還沒來得及寫回記憶體
執行緒b:讀取 m_nValue的當前值,得到值為 0。加1。得到1,但還沒來得及寫回記憶體。
執行緒a:將 1 寫回 m_nValue 記憶體並返回 1。
執行緒b:將 1 寫回 m_nValue記憶體並返回 1。
這種情況源於執行緒間的 interleaving(交叉執行)。
Interleaving 描述了多執行緒同時執行幾句程式碼的各種情況。就算僅僅只有兩個執行緒同時執行這三個操作,也會存在很多可能的 interleaving。當你有許多執行緒同時執行多個操作時,要想枚舉出所有 interleaving,幾乎是不可能的。而且如果執行緒在執行單個操作的不同指令之間被搶佔,也會導致 interleaving 的發生。
目前有許多可以解決這一問題的方案:
- Semaphores
- Atomic references
- Monitors
- Condition codes
- Compare and swap
- ……
本文將使用 Semaphores 去解決這一問題。
事實上,我們僅僅使用了Semaphores 中比較特殊的一種 —— 互斥量。
互斥量是一個特殊的物件,在同一時刻只有一個執行緒能夠得到該物件上的鎖。藉助互斥量這種簡而有力的性質,我們便可以解決執行緒同步問題。
使用互斥量保證 Counter 的執行緒安全
在 C++11 的執行緒庫中,互斥量被放置於標頭檔案 mutex,並以 std::mutex 類加以實現。互斥量有兩個重要的函式:lock() 和 unlock()。顧名思義,前者使當前執行緒嘗試獲取互斥量的鎖,後者則釋放已經獲取的鎖。lock() 函式是阻塞式的,執行緒一旦呼叫 lock(),就會一直阻塞直到該執行緒獲得對應的鎖。
為了使計數器具備執行緒安全性,我們需要對其新增 std::mutex 成員,並在成員函式中對互斥量進行 lock()和unlock() 呼叫。
struct Counter {
int m_nValue;
mutex mtx;
Counter() : m_nValue(0){}
void increment(){
mtx.lock();
++m_nValue;
mtx.unlock();
}
};
如果我們現在再次執行之前的測試程式,我們將始終得到正確的輸出。(加鎖之後,會大大降低程式執行效率,所以可以將上面的內迴圈降低至10000)
異常與鎖
現在讓我們來看看另外一種情況會發生什麼。
假設現在我們的計數器擁有一個 derement() 操作,當 value 被減為 0 時丟擲一個異常:
struct Counter {
int m_nValue;
Counter() : m_nValue(0){}
void increment(){
++m_nValue;
}
void decrement(){
if(m_nValue == 0){
throw "Value cannot be less than 0";
}
--m_nValue;
}
};
假設你想在不更改上述程式碼的前提下為其提供執行緒安全性,那麼你需要為其建立一個 Wrapper 類:
struct ConcurrentCounter{
mutex mtx;
Counter counter;
void increment(){
mtx.lock();
counter.increment();
mtx.unlock();
}
void decrement(){
mtx.lock();
counter.decrement();
mtx.unlock();
}
};
這個 Wrapper 將在大多數情況下正常工作,然而一旦 decrement() 丟擲異常,你就遇到大麻煩了,當異常被丟擲時,unlock() 函式將不會被呼叫,這將導致本執行緒獲得的鎖不被釋放,你的程式也就順理成章的被永久阻塞了。為了修復這一問題,你需要使用 try/catch 塊以保證在丟擲任何異常之前釋放獲得的鎖。
void decrement(){
mtx.lock();
try{
counter.decrement();
} catch(string e) {
mtx.unlock();
throw e;
}
mtx.unlock();
}
程式碼並不複雜,但是看起來卻很醜陋。試想一下,你現在的函式擁有 10 個返回點,那麼你就需要在每個返回點前呼叫 unlock() 函式,而忘掉其中的某一個的可能性是非常大的。更大的風險在於你又添加了新的函式返回點,卻沒有對應地新增 unlock()。下一節將給出解決此問題的好辦法。
鎖的自動管理
當你想保護整個程式碼段(就本文而言是一個函式,但也可以是某個迴圈體或其他控制結構[即一個作用域])免受多執行緒的侵害時,有一個辦法將有助於防止忘記釋放鎖:std::lock_guard。
這個類是一個簡單、智慧的鎖管理器。當 std::lock_guard 例項被建立時,它自動地呼叫互斥量的lock() 函式,當該例項被銷燬時,它也順帶釋放掉獲得的鎖。你可以像這樣使用它:
struct ConcurrentSafeCounter{
mutex mtx;
Counter counter;
void increment(){
lock_guard<mutex> guard(mtx);
counter.increment();
}
void decrement(){
lock_guard<mutex> guard(mtx);
counter.decrement();
}
};
程式碼變得更整潔了不是嗎?
使用這種方法,你無須繃緊神經關注每一個函式返回點是否釋放了鎖,因為這個操作已經被std::lock_guard 例項的解構函式接管了。
注意
現在我們結束了短暫的 Semaphores 之旅。在本章中你學習瞭如何使用 C++ 執行緒庫中的互斥量來保護你的共享資料。**
但有一點請牢記:鎖機制會帶來效率的降低。的確,一旦使用鎖,你的部分程式碼就變得有序[非併發]了。如果你想要設計一個高度併發的應用程式,你將會用到其他一些比鎖更好的機制,但他們已不屬於本文的討論範疇。
C++11 併發程式設計教程 - Part 3 : 鎖的進階與條件變數
上一章使用互斥量解決執行緒同步,這一章將進一步討論互斥量的話題,並介紹C++11併發庫中的另一種同步機制——條件變數。
遞迴鎖
待續。。。
相關推薦
C++11 併發程式設計教程&學習筆記
C++11 併發程式設計教程&學習筆記 為了能夠編譯本文的示例程式碼,你需要有一個支援 C++11 的編譯器,筆者使用的是 TDM-GCC4.9.2 C++11 併發程式設計教程 - Part 1 : thread 初探
Java併發程式設計實戰 - 學習筆記
第2章 執行緒安全性 1. 基本概念 什麼是執行緒安全性?可以這樣理解:一個類在多執行緒環境下,無論執行時環境怎樣排程,無論多個執行緒之間的執行順序是什麼,且在主調程式碼中不需要進行任何額外的同步,如果該類都能呈現出預期的、正確的行為,那麼該類就是執行緒安全的。 既然這樣,那麼安
C++11併發程式設計:原子操作atomic
一:概述 專案中經常用遇到多執行緒操作共享資料問題,常用的處理方式是對共享資料進行加鎖,如果多執行緒操作共享變數也同樣採用這種方式。 為什麼要對共享變數加鎖或使用原子操作?如兩個執行緒操作同一變數過程中,一個執行緒執行過程中可能被核心臨時掛起,這就是執行緒切換,當核心再次切換到該執行緒時,之前的資
C++11 併發程式設計指南——前言
開場白:前一段時間(大概在8月初)開始寫 《C++11 併發程式設計指南》(最早更新於:http://www.cnblogs.com/haippy),但是由於個人能力有限,加上 9 月初到現在一直在忙著找工作(革命尚未成功),精力有限,難免出現錯誤,希望讀者不吝指正。 另外,這是我在併發程式設
[C++11 併發程式設計] 07
假設有兩個執行緒,在執行某些操作時,都需要鎖定一對mutex,執行緒A鎖定了mutex A,而執行緒B鎖定了額mutex B,它們都在等待對方釋放另一個mutex,這就會導致這兩個執行緒都無法繼續執行。這種情況就是死鎖。 避免死鎖最簡單的方法是總是以相同的順序對兩個mute
C++11 併發程式設計基礎(一):併發、並行與C++多執行緒
正文 C++11標準在標準庫中為多執行緒提供了元件,這意味著使用C++編寫與平臺無關的多執行緒程式成為可能,而C++程式的可移植性也得到了有力的保證。另外,併發程式設計可提高應用的效能,這對對效能錙銖必較的C++程式設計師來說是值得關注的。 回到頂部 1. 何為併發 併發指的是兩個或多個獨立的活動在同
[C++11 併發程式設計] 11
上一節,我們瞭解瞭如何對執行緒之間的共享資源進行保護的方法。但是,有些時候,我們需要線上程之間進行同步操作。一個執行緒等待另一個執行緒完成某項工作後,再繼續自己的工作。比如,某個執行緒需要等待一個訊息,或者某個條件變成true。接下來幾節,我們會看到如何使用C++標準庫來做
C++11併發程式設計(一)——初始C++11多執行緒庫
1 前言 C++11標準在標準庫中為多執行緒提供了元件,這意味著使用C++編寫與平臺無關的多執行緒程式成為可能,而C++程式的可移植性也得到了有力的保證。 在之前我們主要使用的多執行緒庫要麼
[C++11 併發程式設計] 17 超時等待
之前我們看到的所有等待機制都是不會超時的,也就是說,等待某個同步事件的執行緒會一直掛起。有些情況下,我們希望設定一個最長等待時間,使得程式可以繼續與使用者進行互動,使得使用者可以取消這個操作。我們先來看看C++11提供的時鐘類clock: clock clock提供瞭如下四
[C++11 併發程式設計] 15 承諾promise
假設有一個應用程式應用程式用於處理大量的網路連線,通常我們會為每一個連線建立單獨的處理執行緒。當執行緒數量較少時,這樣是可行的,但是隨著連線數量的增加,大量的執行緒需要消耗大量的系統資源。這樣,使用較少的執行緒,每個執行緒處理多個連線更為合適。 std::promise&l
[C++11 併發程式設計] 04
C++標準模板庫提供了一個輔助函式 - std::thread::hardware_concurrency(),通過這個函式,我們可以獲取應用程式可以真正併發執行的執行緒數量。下面這個例子,實現了一個併發版本的std::accumulate,它將工作拆分到多個執行緒中,為了
C++深度解析教程學習筆記(3)函數的擴展
插入 分享 技術 lsp 預處理器 _for 返回 忽略 結合 1.內聯函數 1.1.常量與宏的回顧 (1)C++中的 const 常量可以替代宏常數定義,如: const int A = 3; //等價於 #define A 3 (2)C++中是否有解決方案,可以用來
Java程式設計思想學習筆記-第11章
.title { text-align: center; margin-bottom: .2em } .subtitle { text-align: center; font-size: medium; font-weight: bold; margin-top: 0 } .todo { font-famil
C++11併發學習之三:執行緒同步
1.<mutex> 標頭檔案介紹 (1)Mutex系列類(四種) std::mutex,最基本的 Mutex 類。 std::recursive_mutex,遞迴 Mutex 類。 std::time_mutex,定時 Mutex 類。 std::recursive_ti
C++11併發學習之一:小試牛刀
1.與C++11多執行緒相關的標頭檔案 C++11 新標準中引入了四個標頭檔案來支援多執行緒程式設計,他們分別是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。 <at
C++11併發學習之四:執行緒同步(續)
有時候,在第一個執行緒完成前,可能需要等待另一個執行緒執行完成。C++標準庫提供了一些工具可用於這種同步操作,形式上表現為條件變數(condition variable)和期望(future)。 一.條件變數(condition variable) C++標準庫對條件變數有兩套實現:std::c
基於硬體的C(C++)語言程式設計教程11:求解1+2+3+...+100之和
本系列文章希望探討以硬體為平臺講述C(C++)知識的一個新的途徑,改變目前大多數C語言教程僅注重C語言本身的語法規則,而脫離其應用環境的現狀。希望讀者通過本教程的學習,能夠立刻學以致用,真正將所學知識應用到專案實踐中。 開發環境:Atmel Studio 7.0 硬體平臺:Microch
面向物件的程式設計(1)——簡明python教程學習筆記
本文大量內容來源於沈老師的簡明python教程,其中夾雜部分個人的理解如有偏頗之處還望海涵。 一.簡介 到目前為止,在我們的程式中,我們都是根據操作資料的函式或語句塊來設計程式的。這被稱為面向過程的程式設計。 還有一種把資料和功能結合起來,用稱為物
C語言入門教程-學習筆記
變數儲存類別 C語言根據變數的生存週期來劃分,可以分為靜態儲存方式和動態儲存方式。 靜態儲存方式:是指在程式執行期間分配固定的儲存空間的方式。靜態儲存區中存放了在整個程式執行過程中都存在的變數,如全域性變數。 動態儲存方式:是指在程式執行期間根據需要進行動態的分配儲存空
C++視訊教程學習筆記
1. 名稱空間 用於解決命名衝突的問題 裡面可以放函式、變數、結構體、類 可以巢狀 必須定義在全域性作用域下 是開放的,可以隨時往原先的名稱空間中追加內容,而不是覆蓋 實現名稱空間下的函式和呼叫時,需要使用作用域運算子xxx:: 也可以使用無名/匿名名稱空間,相當於和stati