C++ 多線程 (4) 互斥量(mutex)與鎖(lock)


@

一、基本概念

在多線程環境中,有多個線程競爭同一個公共資源,就很容易引發線程安全的問題。因此就需要引入鎖的機制,來保證任意時候只有一個線程在訪問公共資源。

  • 互斥量就是個類對象,可以理解為一把鎖,多個線程嘗試用lock()成員函數來加鎖,只有一個線程能鎖定成功,如果沒有鎖成功,那么流程將卡在lock()這里不斷嘗試去鎖定。
  • 互斥量使用要小心,保護數據不多也不少,少了達不到效果,多了影響效率。

二、使用方法

包含頭文件#include <mutex>

2.1 mutex.lock(),unlock()

步驟:1.lock(),2.操作共享數據,3.unlock()。
lock()和unlock()要成對使用,不能重復上鎖和解鎖。本質就是lock~unlock之間的程序(數據)不會同時調用、修改。

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        my_mutex.lock();
        cout<<"插入數據: "<<num<<endl;
        test_list.push_back(num);
        my_mutex.unlock();
    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        my_mutex.lock();
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"取出數據:"<<tmp<<endl;

        }
        my_mutex.unlock();

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

2.2 std::lock_guard類模板

lock_guard構造函數執行了mutex::lock(),在作用域結束時,自動調用析構函數,執行mutex::unlock()

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        std::lock_guard<std::mutex> my_guard(my_mutex);
        cout<<"插入數據: "<<num<<endl;
        test_list.push_back(num);

    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        std::lock_guard<std::mutex> my_guard(my_mutex);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"取出數據:"<<tmp<<endl;

        }
     

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

2.2.1 std::lock_guard的std::adopt_lock參數

std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);

加入adopt_lock后,在調用lock_guard的構造函數時,不再進行lock();
adopt_guard為結構體對象,起一個標記作用,表示這個互斥量已經lock(),不需要在lock()。

2.3 std::unique_lock函數模板

unique_lock想比於lock_guard,都是基於RAII思想的,也支持std::lock_guard的功能,但是區別在於它提供更多的成員函數,比如:lock(),unlock()使用更加靈活,並且可以和condiction_variable一起使用控制線程同步。但是效率差一點,內存占用多一點。

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        std::unique_lock<std::mutex> my_guard(my_mutex);
        cout<<"插入數據: "<<num<<endl;
        test_list.push_back(num);

    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        std::unique_lock<std::mutex> my_guard(my_mutex);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"取出數據:"<<tmp<<endl;

        }
     

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}


2.3.1 unique_lock的第二個參數

1) std::adopt_lock:

- 表示這個互斥量已經被lock(),即不需要在構造函數中lock這個互斥量了。
- 前提:必須提前lock
- lock_guard中也可以用這個參數

2) std::try_to_lock:

  • 嘗試用mutx的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即返回,不會阻塞在那里,但也不能操作保護的數據(防止異常),只能操作不受保護的數據;
  • 使用try_to_lock的原因是防止其他的線程鎖定mutex太長時間,導致本線程一直阻塞在lock這個地方
  • 前提:不能提前lock();
  • unique_lock.owns_locks()方法判斷是否拿到鎖,如拿到返回true
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<10000;num++){
        std::unique_lock<std::mutex> my_unique(my_mutex, std::try_to_lock);
        if(my_unique.owns_lock()){
            cout<<"插入數據: "<<num<<endl;
            test_list.push_back(num);
        }
        else{
            cout<<"沒能拿到鎖,只能干點別的事"<<endl;
        }


    }

}

void out_list(){

    for(int num=0;num<10000; ++num){


        std::unique_lock<std::mutex> my_unique(my_mutex);
        std::chrono::seconds dura(1);
        std::this_thread::sleep_for(dura);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"取出數據:"<<tmp<<endl;

        }
        else {
            cout<<"已經空了"<<endl;
        }


    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

3) std::defer_lock:

  • 加上defer_lock是始化了一個沒有加鎖的mutex
  • 不給它加鎖的目的是以后可以調用后面提到的unique_lock的一些方法
  • 前提:不能提前lock

2.3.2 unique_lock的成員函數

							(前三個與std::defer_lock聯合使用)

1)lock():加鎖

unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();

作用是:不用自己unlock();
2)unlock():解鎖。

unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//處理一些共享代碼
myUniLock.unlock();
//處理一些非共享代碼
myUniLock.lock();
//處理一些共享代碼

作用:因為一些非共享代碼要處理,可以暫時先unlock(),用其他線程把它們處理了,處理完后再lock()。
3)try_lock():嘗試給互斥量加鎖
如果拿不到鎖,返回false,否則返回true。用法和前面的try_to_lock參數一致。
4)release():釋放unique_lock所管理的mutex對象指針

  • myUniLock(myMutex)相當於把myMutex和myUniLock綁定在了一起,release()就是解除綁定,返回它所管理的mutex對象的指針,並釋放所有權
  • 用法 mutex* ptx =myUniLock.release()
    所有權由ptx接管,如果原來mutex對象處理加鎖狀態,就需要自己進行解鎖了。ptx->unlock();

2.3.3 所有權轉移

  1. 使用move轉移
    unique_lock myUniLock(myMutex);把myMutex和myUniLock綁定在了一起,也就是myUniLock擁有myMutex的所有權
// myUniLock擁有myMutex的所有權,myUniLock可以把自己對myMutex的所有權轉移,但是不能復制。
unique_lock myUniLock2(std::move(myUniLock));
//現在myUniLock2擁有myMutex的所有權。
  1. 在函數中return一個臨時變量,即可以實現轉移
unique_lock<mutex> aFunction()
{
    unique_lock<mutex> myUniLock(myMutex);
    //移動構造函數那里講從函數返回一個局部的unique_lock對象是可以的
    //返回這種局部對象會導致系統生成臨時的unique_lock對象,並調用unique_lock的移動構造函數
    return myUniLock;
}

2.4 std::lock()函數模板

  • std::lock(mutex1,mutex2……):一次鎖定多個互斥量(一般這種情況很少),用於處理多個互斥量。
    如果有一個沒鎖住,就會把已經鎖住的釋放掉,然后它就等待,等所有互斥量都可以同時鎖住,才繼續執行。(要么互斥量都鎖住,要么都沒鎖住,防止死鎖)

三、死鎖

3.1 發生原因

死鎖至少有兩個互斥量mutex1,mutex2。

  1. 線程A執行時,這個線程先鎖mutex1,並且鎖成功了,然后去鎖mutex2的時候,出現了上下文切換。
  2. 線程B執行,這個線程先鎖mutex2,因為mutex2沒有被鎖,即mutex2可以被鎖成功,然后線程B要去鎖mutex1.
  3. 此時,死鎖產生了,A鎖着mutex1,需要鎖mutex2,B鎖着mutex2,需要鎖mutex1,兩個線程沒辦法繼續運行下去。。。

3.2 解決辦法

只要保證多個互斥量上鎖的順序一樣就不會造成死鎖。

四、鎖的效率

lock的代碼段越少,執行越快,整個程序的運行效率越高。

  1. 鎖住的代碼少,叫做粒度細,執行效率高;
  2. 鎖住的代碼多,叫做粒度粗,執行效率低;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM