5 互斥量概念、用法、死鎖演示及解決詳解

5 互斥量概念、用法、死鎖演示及解決詳解

5.1 互斥量(mutex)的基本概念

保護共享數據,操作時,用代碼把共享數據鎖住,操作數據,解鎖,數據被鎖時,其他像操作共享數據必須等待解鎖。

5.2 互斥量的用法

互斥量是個類對象。理解成一把鎖,多個線程嘗試用lock()成員函數來加鎖頭,只有一個線程能夠鎖成功(成功的標誌是lock()函數返回),如果沒鎖成功,那麼流程卡在lock()這裏不斷的嘗試去鎖這把鎖頭。

互斥量使用要小心,保護數據不多也不少,少了,沒達到保護效果,多了,影響效率。

5.2.1 lock(), unlock()

lock()unlock()要成對使用,有lock()必然有unlock(),每調用一次lock(),必然應該調用一次unlock()

代碼5-2-1

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

class A {
public:
    // 把收到的消息(玩家命令)入到一個隊列的線程
    void inMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            cout << "inMsgRecvQueue執行,插入一個元素" << i << endl;
            m_mutex.lock();
            msgRecvQueue.push_back(i); // 假設i就是收到玩家的命令,直接弄到消息隊列裏面
            m_mutex.unlock();
        }
    }
    // 把數據從消息隊列中取出的線程;
    void outMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            m_mutex.lock();
            if (!this->msgRecvQueue.empty()) {
                int command = this->msgRecvQueue.front();
                this->msgRecvQueue.pop_front();
                cout << "outMsgRecvQueue執行,命令爲:" << command << endl;
            } else {
                cout << "outMsgRecvQueue執行,消息隊列爲空" << endl;
            }
            m_mutex.unlock();
        }
    }
private:
    list<int> msgRecvQueue; // 容器,專門用於代表玩家給咱們發送過來的命令。
    mutex m_mutex;
};

int main() {
    A a;
    thread out_t(&A::outMsgRecvQueue, &a); // 第二個參數是 引用, 才能保證線程裏,用的是同一個對象
    thread in_t(&A::inMsgRecvQueue, &a);
    out_t.join();
    in_t.join();
    return 0;
}

運行結果

...
inMsgRecvQueue執行,插入一個元素37
inMsgRecvQueue執行,插入一個元素38
inMsgRecvQueue執行,插入一個元素39
inMsgRecvQueue執行,插入一個元素40
inMsgRecvQueue執行,插入一個元素41
outMsgRecvQueue執行,命令爲:1
inMsgRecvQueue執行,插入一個元素42
inMsgRecvQueue執行,插入一個元素43
inMsgRecvQueue執行,插入一個元素44
...

5.2.2 std::lock_guard類模版

爲了防止編程當中忘記unlock(),引入一個叫std::lock_guard的類模版:你忘記unlock不要緊,我替你unlock

原理:在lock_guard類中,構造函數有lock(),析構函數中有unlock(),對象生命週期結束時,所以會自動調用unlock()

代碼5-2-2

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

class A {
public:
    // 把收到的消息(玩家命令)入到一個隊列的線程
    void inMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            cout << "inMsgRecvQueue執行,插入一個元素" << i << endl;
            m_mutex.lock();
            msgRecvQueue.push_back(i); // 假設i就是收到玩家的命令,直接弄到消息隊列裏面
            m_mutex.unlock();
        }
    }
    // 把數據從消息隊列中取出的線程;
    bool outMsgHelper() {
        lock_guard<mutex> sbguard(m_mutex);
        if (!msgRecvQueue.empty()) {
            int command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            cout << "outMsgRecvQueue執行,命令爲:" << command << endl;
            return true;
        }
        cout << "outMsgRecvQueue執行,消息隊列爲空" << endl;
        return false;
    }
    void outMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            outMsgHelper();
        }
    }
private:
    list<int> msgRecvQueue; // 容器,專門用於代表玩家給咱們發送過來的命令。
    mutex m_mutex;
};

int main() {
    A a;
    thread out_t(&A::outMsgRecvQueue, &a); // 第二個參數是 引用, 才能保證線程裏,用的是同一個對象
    thread in_t(&A::inMsgRecvQueue, &a);
    out_t.join();
    in_t.join();
    return 0;
}

運行結果

...
outMsgRecvQueue執行,消息隊列爲空
outMsgRecvQueue執行,消息隊列爲空
outMsgRecvQueue執行,消息隊列爲空
outMsgRecvQueue執行,消息隊列爲空inMsgRecvQueue執行,插入一個元素
outMsgRecvQueue執行,消息隊列爲空
outMsgRecvQueue執行,消息隊列爲空
outMsgRecvQueue執行,消息隊列爲空
...

爲了增加lock_guard的靈活性,可以用花括號來限制lock_guard對象的生命週期

代碼5-2-3

bool outMsgHelper() {
		{
		...處理其他邏輯
			lock_guard<mutex> sbguard(m_mutex);
      if (!msgRecvQueue.empty()) {
          int command = msgRecvQueue.front();
          msgRecvQueue.pop_front();
          cout << "outMsgRecvQueue執行,命令爲:" << command << endl;
          return true;
      }
      cout << "outMsgRecvQueue執行,消息隊列爲空" << endl;
      return false;
		}
		...處理其他邏輯
}

運行結果

5.3 死鎖

比如我有兩把鎖(死鎖這個問題,是由至少兩個所有也就是兩個互斥量才能產生),金鎖(GoldLock),銀鎖(SilverLock),兩個線程A,B,進行以下過程:

  • 線程A執行的時候,這個線程先鎖GoldLock,把GoldLock鎖成功後,然後去鎖SilverLock
  • 出現了上下文切換。
  • 線程B執行了,這個線程先鎖SilverLock,因爲銀鎖還沒有被鎖,所以銀鎖會lock成功,然後去鎖GoldLock
  • 此時此刻,因爲A鎖不了SilverLock,B鎖不了GoldLock,A和B的流程都走不下去,A和B產生了死鎖。

5.3.1 死鎖的演示

代碼5-3-1

​ A和B鎖的次數要設置得比較多(即代碼中的i變量),不然觀察不到互鎖的現象,並不是不會發生,是發生的概率太小了。

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

class E {
public:
    void A() {
        int i = 1000;
        while(i--) {
            GoldLock.lock();
            SilverLock.lock();
            m_name = "A";
            cout << m_name << endl;
            // 下面的順序無所謂
            SilverLock.unlock();
            GoldLock.unlock();
        }
    }
    void B() {
        int i = 1000;
        while(i--) {
            SilverLock.lock();
            GoldLock.lock();
            m_name = "B";
            cout << m_name << endl;
            GoldLock.unlock();
            SilverLock.unlock();
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

運行結果

...
A
A
A
A
A
A
A
A

執行到最後一行便停止了,產生了死鎖

5.3.2 死鎖的一般解決方案

破壞死鎖的必要條件之循環等待:將系統中的所有資源統一編號,進程可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序(升序)提出。這樣做就能保證系統不出現死鎖。在代碼5-3-1修改爲如下即可:

void A() {
    int i = 1000;
    while(i--) {
        GoldLock.lock();
        SilverLock.lock();
        m_name = "A";
        cout << m_name << endl;
        SilverLock.unlock();
        GoldLock.unlock();
    }
}
void B() {
    int i = 1000;
    while(i--) {
        // 交換了順序
        GoldLock.lock();
        SilverLock.lock();
        m_name = "B";
        cout << m_name << endl;
        // 下面的順序無所謂
        GoldLock.unlock();
        SilverLock.unlock();
    }
}

5.3.3 std::lock()函數模版

能力一次鎖住多個鎖頭(至少兩個,多了不限),在多線程中,它不會因資源的請求順序而產生死鎖。如果有一個互斥量沒鎖住,它就釋放已經請求到的所有資源,過一段時間,再以相同的方式去請求。

代碼5-3-2

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

class E {
public:
    void A() {
        int i = 1000;
        while(i--) {
            lock(GoldLock, SilverLock);
            m_name = "A";
            cout << m_name << endl;
            SilverLock.unlock();
            GoldLock.unlock();
        }
    }
    void B() {
        int i = 1000;
        while(i--) {
            lock(GoldLock, SilverLock);
            m_name = "B";
            cout << m_name << endl;
            GoldLock.unlock();
            SilverLock.unlock();
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

運行結果

...
A
A
A
A
A
A
B
B
...

5.3.4 std::lock_guard的std::adopt_lock參數

因爲用std::lock()解決了死鎖問題,但是我們還需要關心unlock(),要全部解鎖完畢,那怎麼操作才能自動解鎖呢?之前我們用到了std::lock_guard(),剛好這個可以解決我們的問題~

代碼5-3-4

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

class E {
public:
    void A() {
        int i = 10000;
        while(i--) {
            lock(GoldLock, SilverLock);
            lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
            lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
            m_name = "A";
            cout << m_name << endl;
        }
    }
    void B() {
        int i = 10000;
        while(i--) {
            lock(GoldLock, SilverLock);
            lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
            lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
            m_name = "B";
            cout << m_name << endl;
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

運行結果

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