1. 程式人生 > >C++11有關執行緒同步的使用

C++11有關執行緒同步的使用

互斥量和條件變數是控制執行緒同步的常用手段,用來保護多執行緒同時訪問的共享資料。
c++11提供了這些操作,同時還提供了原子變數和一次呼叫的操作,用起來非常的方便。
我們在這裡只介紹如何在C++中使用這些同步機制,有關概念的介紹我們就不在這裡多說了。

互斥量

C++11中提供瞭如下4種語義的互斥量(mutex):

  • std::mutex:獨佔的互斥量,不能遞迴使用。
  • std::timed_mutex:帶超時的獨佔互斥量,不能遞迴使用。
  • std::recursive_mutex:遞迴互斥量,不帶超時功能。
  • std::recursive_timed_mutex:帶超時的遞迴互斥量。

獨佔互斥量std::mutex

這些互斥量的基本介面很相似,一般用法是通過lock()方法來阻塞執行緒,直到獲得互斥量的所有權為止。線上程獲得互斥量並完成任務之後,就必須使用unlock()來解除對互斥量的佔用,lock()和unlock()必須成對出現。try_lock()嘗試鎖定互斥量,如果成功則返回true,如果失敗則返回false,它是非阻塞的。
std::mutex的基本用法程式碼如下:

#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
using namespace std; std::mutex g_lock; void func() { g_lock.lock(); std::cout<<"entered thread "<<std::this_thread::get_id()<<std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout<<"leaving thread "<<std::this_thread::get_id()<<std
::endl; g_lock.unlock(); } int main() { std::thread t1(func); std::thread t2(func); std::thread t3(func); t1.join(); t2.join(); t3.join(); return 0; }

使用lock_guard可以簡化lock/unlock的寫法,同時也更安全,因為lock_guard在構造時會自動鎖定互斥量,而在退出作用域後進行析構時就會自動解鎖,從而保證了互斥量的正確操作,避免忘記unlock操作,因此,應儘量用lokc_guard。lock_guard用到了RAII技術,這種技術在類的建構函式中分配資源,在解構函式中釋放資源,保證資源在出了作用域之後就釋放。上面的例子使用lock_guard後會更簡潔,程式碼如下:

void func()
{
    lock_guard<std::mutex> locker(g_lock);   //出作用域之後自動解鎖
    cout<<"entered thread "<<this_thread::get_id()<<endl;
    this_thread::sleep_for(std::chrono::seconds(1));
    cout<<"leaving thread "<<std::this_thread::get_id()<<endl;
}

遞迴的獨佔互斥量std::recursive_mutex

遞迴鎖允許同一執行緒多次獲得該互斥鎖,可以用來解決同一執行緒需要多次獲取互斥量時死鎖的問題。一個執行緒多次獲取同一個互斥量時會發生死鎖。要解決這個死鎖的問題,一個簡單的辦法就是用遞迴鎖:std::recursive_mutex,它允許同一執行緒多次獲得互斥量。
程式碼示例:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;


struct Complex
{
    std::recursive_mutex mutex;
    int i;

    Complex():i(0){}

    void mul(int x)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i*=x;
    }
    void div(int x)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i/=x;

    }
    void both(int x,int y)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        mul(x);
        div(y);
    }
};

int main()
{
    Complex complex;
    complex.both(32,23);  //因為同一執行緒可以多次獲取同一互斥量,不會發生死鎖

    return 0;
}

需要注意的是儘量不要使用遞迴鎖好,主要原因如下:

  • 需要用到遞迴鎖定的多執行緒互斥處理往往本身就是可以簡化的,允許遞迴互斥很容易放縱複雜邏輯產生,從而導致一些多執行緒同步引起的晦澀問題。
  • 遞迴鎖比起非遞迴鎖,效率會低一些。
  • 遞迴鎖雖然允許同一個執行緒多次獲得同一互斥量,可重複獲得的最大次數並未具體說明,一旦超過一定次數,再對lock進行呼叫就會丟擲std::system錯誤。

帶超時的互斥量

std::timed_mutex是超時的獨佔鎖,std::recursive_timed_mutex是超時的遞迴鎖,主要用在獲取鎖時超時等待功能,因為有時不知道獲取鎖需要多久,為了不至於一直在等待獲取互斥量,就設定一個等待超時時間,在超時後還可以做其他事情。
std::timed_mutex比std::mutex多了兩個超時獲取鎖的介面:try_lock_for和try_lock_until,這兩個介面是用來設定獲取互斥量的超時時間。
std::timed_mutex的基本用法如下程式碼:


#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

std::timed_mutex mutex1;

void work()
{
    chrono::milliseconds timeout(100);

    while(true)
    {
        if(mutex1.try_lock_for(timeout))
        {
            cout<<this_thread::get_id()<<":do work with the mutex"<<endl;
            chrono::milliseconds sleepDuration(250);
            this_thread::sleep_for(sleepDuration);

            mutex1.unlock();
            this_thread::sleep_for(sleepDuration);

        }
        else
        {
            cout<<this_thread::get_id()<<": do work without mutex"<<endl;

            chrono::milliseconds sleepDuration(100);
            this_thread::sleep_for(sleepDuration);
        }
    }
}


int main()
{
    thread t1(work);
    thread t2(work);

    t1.join();
    t2.join();

    return 0;
}

在上面的例子中,通過一個while迴圈不斷地去獲取超時鎖,如果超時還沒有獲取到鎖時就休眠100毫秒,再繼續獲取超時鎖。
相比std::timed_mutex,std::recursive_timed_mutex多了遞迴鎖的功能,允許同一執行緒多次獲得互斥量。

條件變數

條件變數是C++11提供的另外一種用於等待的同步機制,它能阻塞一個或多個執行緒,直到收到另外一個執行緒發出的通知或者超時,才會喚醒當前阻塞的執行緒。條件變數需要和互斥量配合起來用。C++11提供了兩種條件變數:

  • condition_variable,配合std::unique_lock<std::mutex>進行wait操作。
  • condition_variable_any,和任意帶有lock、unlock語義的mutex搭配使用,比較靈活,但效率比condition_variable差一些。

可以看到condition_variable_any比condition_variable更靈活,因為它更通用,對所有的鎖都適用,而condition_variable效能更好。

條件變數的使用過程如下:

  • 擁有條件變數的執行緒獲取互斥量。
  • 迴圈檢查某個條件,如果條件不滿足,則阻塞直到條件滿足;如果條件滿足,則向下執行。
  • 某個執行緒滿足條件執行完之後呼叫notify_one或notify_all喚醒一個或者所有的等待執行緒。

我們可以看一個經典的生產者-消費者的例子:

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<unistd.h>
using namespace std;


mutex m_mutex;   //互斥量
int s=0;         //共享資源
condition_variable m_notempty;   //條件變數

void producer()     //生產者
{
    while(1)
    {
        sleep(1);
        m_mutex.lock();  //上鎖
        s++;
        cout<<"increase one ,s="<<s<<endl;
        m_mutex.unlock();
        m_notempty.notify_one();
    }
}


void consumer()     //消費者
{
    while(1)
    { 
        sleep(1);
        unique_lock<mutex> locker(m_mutex);  
        while(s==0)
        m_notempty.wait(locker);      //使用條件變數
        s--;
        cout<<"decrease one,s="<<s<<endl;


    }
}
int main()
{
    //建立執行緒
    thread thread1(producer);
    thread thread2(consumer);


    thread1.join();
    thread2.join();
}

上面的例子中,while(s==0) m_notempty.wait(locker); ,這句程式碼的意思的是當s等於0的時候,阻塞直到條件滿足時被喚醒。
我們也可以這麼用, m_notempty.wait(locker,[]{return s>0;}); ,將判斷條件放到函式裡面,意思是wait將一直阻塞,知道判斷條件滿足時,被喚醒。

原子變數

C++11提供了一個原子型別std::atomic<T>,可以使用任意型別作為模板引數,C++11內建了整型的原子變數,可以更方便地使用原子變數,使用原子變數就不需要使用互斥量來保護該變量了,因為對該變數的操作保證其是原子的,是不可中斷的。用起來更簡潔。
要做一個計時器,使用mutex時,程式碼如下:


#include<iostream>
#include<mutex>
using namespace std;



struct Counter
{
    public:
    int value;
    std::mutex m_mutex;

    void increment()
    {
        std::lock_guard<std::mutex> lock(mutex);
        value++;
    }

    void decrement()
    {
        std::lock_guard<std::mutex> lock(mutex);
        value--;
    }

    int get()
    {
        return value;
    }
};

如果使用原子變數,就不需要再定義互斥量了,使用更簡便。

#include<iostream>
#include<mutex>
using namespace std;



struct Counter
{
    public:

    std::atomic<int> value;

    void increment()
    {
        value++;
    }

    void decrement()
    {
        value--;
    }

    int get()
    {
        return value;
    }
};

call_one/once_flag的使用

為了保證在多執行緒環境中某個函式僅被呼叫一次,比如,需要初始化某個物件,而這個物件只能初始化一次時,就可以用std::call_once來保證函式在多執行緒環境中只被呼叫一次。使用std::call_once時,需要一個once_flag作為call_one的入參,它的用法比較簡單。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;


std::once_flag flag;

void do_once()
{
    std::call_once(flag,[]{std::cout<<"Called once"<<endl;});
}


int main()
{
    std::thread t1(do_once);
    std::thread t2(do_once);
    std::thread t3(do_once);

    t1.join();
    t2.join();
    t3.join();
}

執行結果:

Called once

相關推薦

C++11有關執行同步的使用

互斥量和條件變數是控制執行緒同步的常用手段,用來保護多執行緒同時訪問的共享資料。 c++11提供了這些操作,同時還提供了原子變數和一次呼叫的操作,用起來非常的方便。 我們在這裡只介紹如何在C++中使用這些同步機制,有關概念的介紹我們就不在這裡多說了。

C++11執行同步

多執行緒能提高程式的效率,但同時也帶來了相應的問題----資料競爭。當多個執行緒同時操作同一個變數時,就會出現資料競爭。出現數據競爭,一般會用臨界區(Critical Section)、互斥量(Mutex)、訊號量(Semaphore)、事件(Event)這四種方法來完

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執行 future/promise

[轉自 https://blog.csdn.net/jiange_zh/article/details/51602938] 1. < future >標頭檔案簡介 Classes std::future std::future_error std::packaged_task std::pro

c++11執行 thread

 1.thread建構函式 default (1) thread() noexcept; initialization (2) template <class Fn, class... Args> explicit

C++11執行執行共享資料

共享資料的問題 這些在作業系統中都有詳細的介紹,可以回顧作業系統課程。。很典型的就是資料競爭問題。 互斥量保護資料 最原始的方式:使用std::mutex建立互斥量,使用成員lock()加鎖,使用成員unlock()解鎖。但是這種方式需要我們在每個函數出口都呼叫一次unloc

基於C++11實現執行池的工作原理.

基於C++11實現執行緒池的工作原理. 文章目錄 基於C++11實現執行緒池的工作原理. 簡介 執行緒池的組成 1、執行緒池管理器 2、工作執行緒 3、任務介面, 4、任務佇列

基於C++11實現執行池的工作原理

基於C++11實現執行緒池的工作原理. 不久前寫過一篇執行緒池,那時候剛用C++寫東西不久,很多C++標準庫裡面的東西沒怎麼用,今天基於C++11重新實現了一個執行緒池。 簡介 執行緒池(thread pool):一種執行緒的使用模式,執行緒過多會帶來排程開銷,進而影響快取區域性性和整體效能。而執行緒池

c++11執行執行

最近需要開發一個高效能運算庫,涉及到c++多執行緒的應用,上次做類似的事情已經是4年多以前了,印象中還頗有些麻煩。悔當初做了就算了,也沒想著留點記錄什麼的。這次又研究了一番,發現用上c++11特性之後,現在已經比較簡單了,在此記錄一下。   最簡單的多執行緒情況,不涉及公共變數,各個執行緒之間獨

利用C++11實現執行task的簡單封裝

#include <functional> #include <thread> #include <type_traits> /*Compile only if 'F' is callable. F maybe function, la

C++11執行(1)

        C++11中添加了duox多執行緒類,編寫C++程式可以直接使用C++11中的多執行緒庫,不必依賴於平臺多執行緒,這樣可以方便寫出誇平臺的多執行緒程式。多執行緒可以最大化利用計算機資源,提高程式碼的執行效率。         C++11中thread類提供兩

C++ 11執行下std::unique_lock與std::lock_guard的區別和用法

這裡主要介紹std::unique_lock與std::lock_guard的區別用法 先說簡單的 一、std::lock_guard的用法 std::lock_guard其實就是簡單的RAII封裝,在建構函式中進行加鎖,解構函式中進行解鎖,這樣可以保證函式退出時,鎖一定被釋放。 簡單來說,就是防止開

基於C++11執行

*需要C++11的支援,在vs2013下編譯通過 執行效果 背景 在傳統的收到任務即建立執行緒的情況下,我們每收到一個任務,就建立一個執行緒,執行任務,銷燬執行緒, 我們把這三個過程所用的時間分別記做T1,T2,T3 任務本身所用的時間僅佔T2/(T1+T2+T3),這在任務本身所用時間很短的情況下

基於C++11執行池(threadpool),簡潔且可以帶任意多的引數(轉)

咳咳。C++11 加入了執行緒庫,從此告別了標準庫不支援併發的歷史。然而 c++ 對於多執行緒的支援還是比較低階,稍微高階一點的用法都需要自己去實現,譬如執行緒池、訊號量等。執行緒池(thread pool)這個東西,在面試上多次被問到,一般的回答都是:“管理一個任務佇列,一個執行緒佇列,然後每次取一個任務分

C++11執行------std::async

std::async可以認為是封裝了一個std::promise,該函式返回一個std::future,用於獲取其他執行緒的資料。 一般有兩種模式: std::lanch::async:最常用的非同步模式,每次都要執行一遍 std::lanch::defer:只在第

C++11執行---互斥量、鎖、條件變數的總結

關於互斥量std::mutex的總結 互斥量用於組成程式碼的臨界區。C++的多執行緒模型是基於記憶體的,或者說是基於程式碼片段的,這和我們作業系統學習的臨界區概念基本一致,但是與Golang不同,Golang是基於訊息模型的。 一個std::mutex的lock()和unlock

C++11執行的原子操作

原子操作是同時只能有一個執行緒執行一個操作,不用使用互斥量即可實現,但是速度慢,而且一般只支援原生的型別,不夠靈活。更多的用處是作為訊號量進行使用。 示例程式碼,以int為例子: #include <atomic> #include <thread> #i

C++ 11執行--執行管理

說到多執行緒程式設計,那麼就不得不提並行和併發,多執行緒是實現併發(並行)的一種手段。並行是指兩個或多個獨立的操作同時進行。注意這裡是同時進行,區別於併發,在一個時間段內執行多個操作。在單核時代,多個執行緒是併發的,在一個時間段內輪流執行;在多核時代,多個執行緒可以實現真正的並行,在多核上真正獨立的並行執行。

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

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

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 &