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