C++11有關線程同步的使用

原文鏈接:https://blog.csdn.net/fengxinlinux/article/details/76686829

C++11有關線程同步的使用

 

本文鏈接:https://blog.csdn.net/fengxinlinux/article/details/76686829

互斥量和條件變量是控制線程同步的常用手段,用來保護多線程同時訪問的共享數據。
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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

使用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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

遞歸的獨佔互斥量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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

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

  • 需要用到遞歸鎖定的多線程互斥處理往往本身就是可以簡化的,允許遞歸互斥很容易放縱複雜邏輯產生,從而導致一些多線程同步引起的晦澀問題。
  • 遞歸鎖比起非遞歸鎖,效率會低一些。
  • 遞歸鎖雖然允許同一個線程多次獲得同一互斥量,可重複獲得的最大次數並未具體說明,一旦超過一定次數,再對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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

在上面的例子中,通過一個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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

上面的例子中,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;
    }
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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

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



struct Counter
{
    public:

    std::atomic<int> value;

    void increment()
    {
        value++;
    }

    void decrement()
    {
        value--;
    }

    int get()
    {
        return value;
    }
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

 

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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

運行結果:

Called once

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章