C++併發編程2——爲保護數據加鎖(一)

找到問題的解決辦法,而不是找蹩腳的接口。

在應屆生面試的時候,很多面試官都會問——“多線程如何共享資源”。在操作系統層面上可以給出若干關鍵詞答案,但是在語言層面,這個問題考慮的就沒有那麼簡單了。同時,很多人會將多線程數據共享和線程同步混淆。有關線程同步,我們會在接下來的章節裏着重闡述。本文主要聚焦於保護共享數據,首先從加鎖入手,進而擴展到加鎖無法解決的問題,最後會給出一些其他保護方案。

參數入棧


一個存放參數的棧數據結構,相同函數的參數必須要在棧中相連,我們來實現這個功能,看下面代碼:

#include <stack>
#include <iostream>

class MutexTest
{
public:
    MutexTest(): m_charStack() { }
    ~MutexTest() { }

    void Push(int n, char c)
    {
        for (int i = 0; i < n; ++i)
        {
            m_charStack.push(c);
            std::cout << c;
        }
        std::cout << std::endl;
    }
private:
    std::stack<char> m_charStack;
};

MutexTest test;
std::thread mutexTestThread1(&MutexTest::Push, &test, 10, 'a');
std::thread mutexTestThread2(&MutexTest::Push, &test, 10, 'b');

mutexTestThread1.join();
mutexTestThread2.join();

上面這段代碼的執行結果是不確定的,這是因爲我們無法預測線程的執行順序,多個線程共享同一個數據棧存在競態條件(Race Condition)。所以我們可能得到下面的執行結果,所有的參數都是交叉在一起的,這不是我們想要的結果。

aabbbbbbbaaaaaaaabbb

競態條件是多線程編程的噩夢,爲什麼會出現競態條件可以自行百度,我們主要是爲了解決這個問題。讓最終執行的結果爲:

aaaaaaaaaa
bbbbbbbbbb

參數入棧保護


std::mutex是C++11提供的數據加鎖類,C++中通過實例化 std::mutex 創建互斥量,通過調用成員函數lock()進行上鎖,unlock()進行解鎖。

class MutexTest
{
public:
    MutexTest(): m_mutex(), m_charStack() { }
    ~MutexTest() { }

    void Push(int n, char c)
    {
        m_mutex().lock();
        for (int i = 0; i < n; ++i)
        {
            m_charStack.push(c);
            std::cout << c;
        }
        std::cout << std::endl;
        m_mutex().unlock();
    }
private:
    std::mutex       m_mutex;
    std::stack<char> m_charStack;
};

這段代碼和上面的不同點就是使用std::mutex,在訪問m_charStack之前上鎖,其他線程就必須要等待解鎖後才能訪問m_charStack。如果我們忘記解鎖,那麼m_charStack就再也無法被訪問了,所以有必要用RAII類std::lock_guard進行封裝——構造時上鎖,析構時解鎖。

void MutexTest::Push(int n, char c)
{
    std::lock_guard<std::mutex> lg(m_mutex);
    for (int i = 0; i < n; ++i)
    {
        m_charStack.push(c);
        std::cout << c;
    }
    std::cout << std::endl;
}

C++還提供了std::unique_lock鎖,相對於std::lock_guard,該鎖提供了更好地上鎖和解鎖靈活性控制。std::unique_lock以獨佔所有權的方式來管理mutex對象的上鎖和解鎖操作。我們來看看其用法

// unique_lock constructor example
#include <iostream>   
#include <thread>   
#include <mutex>      

std::mutex foo,bar;

void task_a () {
  std::lock (foo,bar);         // simultaneous lock (prevents deadlock)
  std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
  std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
  std::cout << "task a\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b () {
  // foo.lock(); bar.lock(); // replaced by:
  std::unique_lock<std::mutex> lck1, lck2;
  lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
  lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
  std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)
  std::cout << "task b\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}


int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

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

  return 0;
}

現在我們終於得到了我們想要的結果,可惜在很多時候加鎖並不是解決數據共享的萬能藥。下一節,我們將會涉及到一些加鎖無法解決的數據共享問題。


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