1. 程式人生 > >C++11 併發程式設計教程&學習筆記

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變數。

當單執行緒執行上述程式碼,每次執行的結果是一樣的,上述三個步驟會按順序進行。但是在多程序情況下,可能存在如下執行順序:

  1. 執行緒a:讀取 m_nValue的當前值,得到值為 0。加1。得到1,但還沒來得及寫回記憶體

  2. 執行緒b:讀取 m_nValue的當前值,得到值為 0。加1。得到1,但還沒來得及寫回記憶體。

  3. 執行緒a:將 1 寫回 m_nValue 記憶體並返回 1。

  4. 執行緒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

基於硬體的CC++)語言程式設計教程11:求解1+2+3+...+100之和

本系列文章希望探討以硬體為平臺講述C(C++)知識的一個新的途徑,改變目前大多數C語言教程僅注重C語言本身的語法規則,而脫離其應用環境的現狀。希望讀者通過本教程的學習,能夠立刻學以致用,真正將所學知識應用到專案實踐中。 開發環境:Atmel Studio 7.0 硬體平臺:Microch

面向物件的程式設計(1)——簡明python教程學習筆記

本文大量內容來源於沈老師的簡明python教程,其中夾雜部分個人的理解如有偏頗之處還望海涵。 一.簡介 到目前為止,在我們的程式中,我們都是根據操作資料的函式或語句塊來設計程式的。這被稱為面向過程的程式設計。 還有一種把資料和功能結合起來,用稱為物

C語言入門教程-學習筆記

變數儲存類別 C語言根據變數的生存週期來劃分,可以分為靜態儲存方式和動態儲存方式。 靜態儲存方式:是指在程式執行期間分配固定的儲存空間的方式。靜態儲存區中存放了在整個程式執行過程中都存在的變數,如全域性變數。 動態儲存方式:是指在程式執行期間根據需要進行動態的分配儲存空

C++視訊教程學習筆記

1. 名稱空間 用於解決命名衝突的問題 裡面可以放函式、變數、結構體、類 可以巢狀 必須定義在全域性作用域下 是開放的,可以隨時往原先的名稱空間中追加內容,而不是覆蓋 實現名稱空間下的函式和呼叫時,需要使用作用域運算子xxx:: 也可以使用無名/匿名名稱空間,相當於和stati