1. 程式人生 > >C++11多執行緒程式設計系列(二)實戰

C++11多執行緒程式設計系列(二)實戰

C++11 新標準中引入了多個頭檔案來支援多執行緒程式設計,他們分別是<atomic> ,<thread>,<mutex>,<condition_variable><future>

<atomic>:該頭文主要聲明瞭兩個類, std::atomic 和 std::atomic_flag,另外還聲明瞭一套 C 風格的原子型別和與 C 相容的原子操作的函式。
<thread>:該標頭檔案主要聲明瞭 std::thread 類,另外 std::this_thread 名稱空間也在該標頭檔案中。
<mutex>

:該標頭檔案主要聲明瞭與互斥量(mutex)相關的類,包括 std::mutex 系列類,std::lock_guard, std::unique_lock, 以及其他的型別和函式。
<condition_variable>:該標頭檔案主要聲明瞭與條件變數相關的類,包括 std::condition_variable 和 std::condition_variable_any。
<future>:該標頭檔案主要聲明瞭 std::promise, std::package_task 兩個 Provider 類,以及 std::future 和 std::shared_future 兩個 Future 類,另外還有一些與之相關的型別和函式,std::async() 函式就宣告在此標頭檔案中。

C++ 11保證區域性static只會被一個執行緒初始化

thread

執行緒函式不僅支援普通函式,還可以是類的成員函式和lambda表示式

可被 joinable 的 thread 物件必須在他們銷燬之前被主執行緒 join 或者將其設定為 detached.

執行緒不像程序,一個程序中的執行緒之間是沒有父子之分的,都是平級關係。即執行緒都是一樣的, 退出了一個不會影響另外一個。但是所謂的”主執行緒”main,其入口程式碼是類似這樣的方式呼叫main的:exit(main(…))。main執行完之後, 會呼叫exit()。exit() 會讓整個程序over終止,那所有執行緒自然都會退出。

如果程序中的任一執行緒呼叫了exit,_Exit或者_exit,那麼整個程序就會終止。

move操作是將一個程序轉移給另一個程序,注意程序只能被轉移不能被複制。也可以用swap交換兩個執行緒。參考:C++ 11 thread join detach move swap

threads[i] = std::thread(thread_task, i + 1);這種做法是可以的,執行緒是不可以進行復制的。(這裡可能是右值引用的方式過載了=操作符)

std::mutex

最簡單的用法:

#include<mutex>
std::mutex g_mutex; //全域性
g_mutex.lock();
......
g_mutex.unlock();

但是,如果在上鎖和解鎖之間出現異常導致沒有執行解鎖操作,那其他執行緒就永遠無法得到鎖。因此使用std::lock_guard:

#include<mutex>
std::mutex g_mutex;//全域性
std::lock_guard<std::mutex> lg(g_mutex);//某個執行緒函式內,構造的時候上鎖,析構的時候解鎖

std::lock_guard限制得太死了,只有構造和解構函式,沒法通過它的成員函式加鎖和解鎖。為此,C++11提供了靈活的std:unique_lock模板類。std::unique_lock提供lock和unlock函式,因此可以在適當的時候加解鎖。這樣可以降低鎖的粒度。預設情況下,std::unique_lock的建構函式會對mutex進行加鎖,在析構的時候會對mutex進行解鎖:

#include<mutex>
std::unique_lock<std::mutex> ul(g_mutex);//建構函式進行上鎖
......
ul.unlock();//解鎖,降低鎖的粒度
......
ul.lock();
......
//解構函式會進行解鎖
  std::unique_lock<std::mutex> ul1(m_mutex, std::defer_lock); //延遲上鎖

  std::unique_lock<std::mutex> ul1(m_mutex, std::adopt_lock);//已經上鎖

延遲上鎖是指在建構函式裡面不需要給它上鎖。已經上鎖是表示在構造之前就已經上鎖了。
std::lock_guard也是支援std::adopt_lock的,但不支援std::defer_lock,估計是因為std::lock_guard內部變數記錄鎖的狀態,它只知道在建構函式加鎖(或者由adopt_lock指明無需加鎖),在解構函式解鎖。

對於使用了std::defer_lock的std::unique_lock,以後手動加鎖時要通過std::unique_lock類的lock()函式,而不用std::mutex的lock()函式,因為std::unique_lock需要記錄mutex的加鎖情況。

C++11提供了一個模板函式std::lock()使得很容易原子地對多個鎖進行加鎖。std::lock函式只要求引數有lock操作即可,也就是說可以傳一個std::mutex或者std::unique_lock變數給std::lock。std::lock_guard變數則不行,因為其沒有lock()函式。

template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
std::lock(ul1, ul2);//同時對多個鎖上鎖

condition_variable

C++裡面使用條件變數實現和訊號量相同的功能。下面程式碼是一個經典的生產者消費者模型:

#include<thread>
#include<iostream>
#include<mutex>
#include<list>
#include<condition_variable>

std::mutex g_mutex;
std::condition_variable cond;

std::list<int> alist;

void threadFun1()
{
    std::unique_lock<std::mutex> ul(g_mutex);
    while (alist.empty())
    {
        cond.wait(ul);
    }

    std::cout << "threadFun1 get the value : " << alist.front() << std::endl;
    alist.pop_front();
}

void threadFun2()
{
    std::lock_guard<std::mutex> lg(g_mutex);
    alist.push_back(13);

    cond.notify_one();
}

int main()
{
    std::thread th1(threadFun1);
    std::thread th2(threadFun2);

    th1.join();
    th2.join();

    return 0;
}

上面例子之所以用一個while迴圈而不是if,是因為存在虛假喚醒情景。當notify激活了多個執行緒之後,如果某個執行緒率先拿到鎖將資料取空,其他執行緒應該再次檢查一下資料是否為空。

std::condition_variable 提供了兩種 wait() 函式。當前執行緒呼叫 wait() 後將被阻塞(此時當前執行緒應該獲得了鎖(mutex),不妨設獲得鎖 lck),直到另外某個執行緒呼叫 notify_* 喚醒了當前執行緒。
線上程被阻塞時,該函式會自動呼叫 lck.unlock() 釋放鎖,使得其他被阻塞在鎖競爭上的執行緒得以繼續執行。另外,一旦當前執行緒獲得通知(notified,通常是另外某個執行緒呼叫 notify_* 喚醒了當前執行緒),wait() 函式也是自動呼叫 lck.lock(),使得 lck 的狀態和 wait 函式被呼叫時相同

future

目的是為了獲得執行緒函式的返回值,如果使用join的方法,主執行緒等待次執行緒結束後,再去讀取全域性變數即可。但是join是等待次執行緒結束,而結束有很多種原因,比如正常結束和拋異常提前終止。對於後者,並不能保證join返回後,讀取全域性變數得到的就是所要的值。

#include<thread>
#include<iostream>
#include<mutex>
#include<vector>
#include<future>
#include<numeric>

void threadFun(const std::vector<int> &big_vec, std::promise<double> prom)
{
    double sum = std::accumulate(big_vec.begin(), big_vec.end(), 0.0);

    double avg = 0;
    if (!big_vec.empty())
        avg = sum / big_vec.size();

    prom.set_value(avg);
}

int main()
{
    std::promise<double> prom;
    std::future<double> fu = prom.get_future();

    std::vector<int> vec{ 1, 2, 3, 4, 5, 6 };
    //以右值引用的方式進行傳遞,本執行緒中的prom物件轉移給了子執行緒,保證主執行緒不會一直阻塞。
    std::thread th(threadFun, std::ref(vec), std::move(prom));
    th.detach();

    double avg = fu.get();//阻塞一直到次執行緒呼叫set_value

    std::cout << "avg = " << avg << std::endl;

    return 0;
}

如果在析構std::promise變數時,還沒對std::pormise變數進行設定,那麼解構函式就會為其關聯的std::future儲存一個std::future_error異常。此時,std::future的get()函式會丟擲一個std::futre_error異常。

std::future是一次性的。std::promise只能呼叫一次get_future,std::future也只能呼叫一次get()。 如果想在多個執行緒中共享一個std::promise的設定值,可以使用std::shared_future。

有了std::packaged_task,執行緒函式就可以直接返回一個值。這樣顯得更加自然。從下面例子也可以看到,std::packaged_task並非一定要作為std::thread的引數,它完全可以在主執行緒中呼叫。

#include <iostream>
#include <cmath>
#include <thread>
#include <future>
#include <functional>

// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x,y); }

void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();

    task(2, 9);

    std::cout << "task_lambda:\t" << result.get() << '\n';
}

void task_bind()
{
    std::packaged_task<int()> task(std::bind(f, 2, 11));
    std::future<int> result = task.get_future();

    task();

    std::cout << "task_bind:\t" << result.get() << '\n';
}

void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();

    std::thread task_td(std::move(task), 2, 10);
    task_td.join();

    std::cout << "task_thread:\t" << result.get() << '\n';
}

int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

再用async進行一層封裝:

#include<thread>
#include<iostream>
#include<vector>
#include<future>
#include<numeric>

double calcAvg(const std::vector<int> &vec)
{
    double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
    double avg = 0;
    if (!vec.empty())
        avg = sum / vec.size();

    return avg;
}


int main()
{
    std::vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::future<double> fu = std::async(calcAvg, std::ref(vec));

    double avg = fu.get();
    std::cout << "avg = " << avg << std::endl;

    return 0;
}

atomic

個人理解,原子操作在效能上優與互斥量,當對某一種資料型別執行原子操作時,它會更多地使用物理機器上提供的原子操作,避免執行緒阻塞。如果不可以的話,可能內部會用自旋鎖來解決同步問題。(原子操作,要麼該執行緒執行完該操作,要麼該操作都不執行)

最基本的是std::atomic_flag ,不過使用更多的是std::atomic,這還針對整型和指標做了模板特化。

在使用atomic時,會涉及到記憶體模型的概念。順序一致性模型不僅在共享儲存系統上適用,在多處理器和多執行緒環境下也同樣適用。而在多處理器和多執行緒環境下理解順序一致性包括兩個方面,(1). 從多個執行緒平行角度來看,程式最終的執行結果相當於多個執行緒某種交織執行的結果,(2)從單個執行緒內部執行順序來看,該執行緒中的指令是按照程式事先已規定的順序執行的(即不考慮執行時 CPU 亂序執行和 Memory Reorder)。

我們在執行我們的程式碼時,首先會經過編譯器優化(可能會生成打亂順序的組合語言),CPU也可能會亂序執行指令以實現優化。記憶體模型對編譯器和 CPU 作出一定的約束才能合理正確地優化你的程式。

自旋鎖

互斥鎖得不到鎖時,執行緒會進入休眠,這類同步機制都有一個共性就是 一旦資源被佔用都會產生任務切換,任務切換涉及很多東西的(儲存原來的上下文,按排程演算法選擇新的任務,恢復新任務的上下文,還有就是要修改cr3暫存器會導致cache失效)這些都是需要大量時間的,因此用互斥之類來同步一旦涉及到阻塞代價是十分昂貴的。

一個互斥鎖來控制2行程式碼的原子操作,這個時候一個CPU正在執行這個程式碼,另一個CPU也要進入, 另一個CPU就會產生任務切換。為了短短的兩行程式碼 就進行任務切換執行大量的程式碼,對系統性能不利,另一個CPU還不如直接有條件的死迴圈,等待那個CPU把那兩行程式碼執行完。

當鎖被其他執行緒佔有時,獲取鎖的執行緒便會進入自旋,不斷檢測自旋鎖的狀態。一旦自旋鎖被釋放,執行緒便結束自旋,得到自旋鎖的執行緒便可以執行臨界區的程式碼。對於臨界區的程式碼必須短小,否則其他執行緒會一直受到阻塞,這也是要求鎖的持有時間儘量短的原因!

讀寫鎖

讀寫鎖和互斥量(互斥鎖)很類似,是另一種執行緒同步機制,但不屬於POSIX標準,可以用來同步同一程序中的各個執行緒。當然如果一個讀寫鎖存放在多個程序共享的某個記憶體區中,那麼還可以用來進行程序間的同步,

和互斥量不同的是:互斥量會把試圖進入已保護的臨界區的執行緒都阻塞;然而讀寫鎖會視當前進入臨界區的執行緒和請求進入臨界區的執行緒的屬性來判斷是否允許執行緒進入。

相對互斥量只有加鎖和不加鎖兩種狀態,讀寫鎖有三種狀態:讀模式下的加鎖,寫模式下的加鎖,不加鎖。

讀寫鎖的使用規則:

只要沒有寫模式下的加鎖,任意執行緒都可以進行讀模式下的加鎖;
只有讀寫鎖處於不加鎖狀態時,才能進行寫模式下的加鎖;
讀寫鎖也稱為共享-獨佔(shared-exclusive)鎖,當讀寫鎖以讀模式加鎖時,它是以共享模式鎖住,當以寫模式加鎖時,它是以獨佔模式鎖住。讀寫鎖非常適合讀資料的頻率遠大於寫資料的頻率從的應用中。這樣可以在任何時刻執行多個讀執行緒併發的執行,給程式帶來了更高的併發度。

參考及相關博文

相關推薦

C++11執行程式設計系列實戰

C++11 新標準中引入了多個頭檔案來支援多執行緒程式設計,他們分別是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。 <

C++11併發/執行程式設計系列2

std::thread詳解 std::thread在標頭檔案<thread>中宣告,因此使用 std::thread 時需要包含 <thread>標頭檔案。 default(1) thread() noexcept;

c++11執行詳解

  原文作者:aircraft 原文連結:https://www.cnblogs.com/DOMLX/p/10914162.html              最近是恰好寫了一些c++11多執行緒有關的東西,就寫一下筆記留著以後自己忘記回來看吧,也不是專門寫給讀者看的,我就想到哪就寫到哪吧 &nbs

c++11 執行入門教程

  原文作者:aircraft 原文連結:https://www.cnblogs.com/DOMLX/p/10945309.html              最近是恰好寫了一些c++11多執行緒有關的東西,就寫一下筆記留著以後自己忘記回來看吧,也不是專門寫給讀者看的,我就想到哪就寫到哪吧

執行程式設計總結——條件變數和互斥鎖

#include <stdio.h> #include <pthread.h> #include <error.h> #include <assert.h> #include <stdlib.h> typedef int DataType; typ

c++11執行程式設計:joining和detaching 執行

Joining執行緒 執行緒一旦啟動,另一個執行緒可以通過呼叫std::thread物件上呼叫join()函式等待這個執行緒執行完畢std::thread th(funcPtr); th.join(); 看一個例子主執行緒啟動10個工作執行緒,啟動完畢後,main函式等待

c++11執行程式設計:資料共享和競爭條件

在多執行緒環境中,執行緒間的資料共享很簡單,但是在程式中這種簡單的資料共享可能會引起問題,其中一種便是競爭條件。什麼是競爭條件? 競賽條件是發生在多執行緒應用程式中的一種bug 當兩個或多個執行緒並行執行一組操作,訪問相同的記憶體位置,此時,它們中的一個或多個執行緒會修改記

c++11執行程式設計:建立執行的三種方法

c++11執行緒庫原始的c++標準僅支援單執行緒程式設計,新的c++標準(c++11或c++0x)於2011年釋出,引入了新的執行緒庫。 編譯器要求 Linux: gcc 4.8.1 (完全併發支援) Windows: Visual Studio 2012 and Min

C++11執行程式設計 緒論及總結

C++11多執行緒程式設計 這一系列文章是從 https://thispointer.com/c11-multithreading-tutorial-series/ 轉過來的, 本來想翻譯一下, 但看了些內容, 用詞都不難, 讀英文沒有太大難度, 翻譯過來反而怕用詞不準畫蛇添

C++11執行程式設計 第十章: 使用packaged_task優雅的讓同步函式非同步執行

C++11 Multithreading – Part 10: packaged_task<> Example and Tutorial Varun July 2, 2017 C++11 Multithreading – Part 10: packaged_tas

C++11執行程式設計 第九章: std::async 更更優雅的寫執行

C++11 Multithreading – Part 9: std::async Tutorial & Example Varun May 5, 2017 C++11 Multithreading – Part 9: std::async Tutorial &

C++11執行程式設計 第八章: 使用 std::future std::promise 更優雅的獲取執行返回值

C++11 Multithreading – Part 8: std::future , std::promise and Returning values from Thread Varun June 20, 2015 C++11 Multithreading – Part

C++11執行程式設計 第七章: 條件變數及其使用方法

C++11 Multithreading – Part 7: Condition Variables Explained Varun June 2, 2015 C++11 Multithreading – Part 7: Condition Variables Explain

C++11執行程式設計 第五章: 使用鎖來解決竟態條件

C++11 Multithreading – Part 5: Using mutex to fix Race Conditions Varun February 22, 2015 C++11 Multithreading – Part 5: Using mutex to fi

C++11執行程式設計 第四章: 共享資料和競態條件

C++11 Multithreading – Part 4: Data Sharing and Race Conditions Varun February 21, 2015C++11 Multithreading – Part 4: Data Sharing and Race Con

C++11執行程式設計 第三章: 如何向執行傳參

C++11 Multithreading – Part 3: Carefully Pass Arguments to Threads Varun January 22, 2015 C++11 Multithreading – Part 3: Carefully Pass Ar

C++11執行程式設計 第二章: join 和 detach 執行

  C++11 Multithreading – Part 2: Joining and Detaching Threads Varun January 21, 2015 C++11 Multithreading – Part 2: Joining and De

C++11執行程式設計 第六章: 執行之間的事件處理

C++11 Multithreading – Part 6: Need of Event Handling Varun June 1, 2015 C++11 Multithreading – Part 6: Need of Event Handling2018-08-18T1

C++11 執行程式設計使用例項

最新研究下C++11中執行緒的知識,基本內容如下: 1、C++11中建立執行緒的幾種方式 在C11中,我們可以通過建立std::thread類的物件來建立額外的執行緒。每個thread物件可以跟具體的某個執行緒關聯,從而達到多執行緒併發的目的。  必須 #include 

C++11執行程式設計基礎入門

1.在C++11中建立新執行緒   在每個c++應用程式中,都有一個預設的主執行緒,即main函式,在c++11中,我們可以通過建立std::thread類的物件來建立其他執行緒,每個std :: thread物件都可以與一個執行緒相關聯,只需包含標頭檔案&l