探究在C++程序并發(fā)時(shí)保護(hù)共享數(shù)據(jù)的問題
我們先通過一個(gè)簡單的代碼來了解該問題。
同步問題
我們使用一個(gè)簡單的結(jié)構(gòu)體 Counter,該結(jié)構(gòu)體包含一個(gè)值以及一個(gè)方法用來改變這個(gè)值:
struct Counter {
int value;
void increment(){
++value;
}
};
然后啟動多個(gè)線程來修改結(jié)構(gòu)體的值:
int main(){
Counter counter;
std::vector<std::thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(std::thread([&counter](){
for(int i = 0; i < 100; ++i){
counter.increment();
}
}));
}
for(auto& thread : threads){
thread.join();
}
std::cout << counter.value << std::endl;
return 0;
}
我們啟動了5個(gè)線程來增加計(jì)數(shù)器的值,每個(gè)線程增加了100次,然后在線程結(jié)束時(shí)打印計(jì)數(shù)器的值。
但我們運(yùn)行這個(gè)程序的時(shí)候,我們是希望它會答應(yīng)500,但事實(shí)不是如此,沒人能確切知道程序?qū)⒋蛴∈裁唇Y(jié)果,下面是在我機(jī)器上運(yùn)行后打印的數(shù)據(jù),而且每次都不同:
442 500 477 400 422 487
問題的原因在于改變計(jì)數(shù)器值并不是一個(gè)原子操作,需要經(jīng)過下面三個(gè)操作才能完成一次計(jì)數(shù)器的增加:
- 首先讀取 value 的值
- 然后將 value 值加1
- 將新的值賦值給 value
但你使用單線程來運(yùn)行這個(gè)程序的時(shí)候當(dāng)然沒有任何問題,因此程序是順序執(zhí)行的,但在多線程環(huán)境中就有麻煩了,想象下下面這個(gè)執(zhí)行順序:
- Thread 1 : 讀取 value, 得到 0, 加 1, 因此 value = 1
- Thread 2 : 讀取 value, 得到 0, 加 1, 因此 value = 1
- Thread 1 : 將 1 賦值給 value,然后返回 1
- Thread 2 : 將 1 賦值給 value,然后返回 1
這種情況我們稱之為多線程的交錯(cuò)執(zhí)行,也就是說多線程可能在同一個(gè)時(shí)間點(diǎn)執(zhí)行相同的語句,盡管只有兩個(gè)線程,交錯(cuò)的現(xiàn)象也很明顯。如果你有更多的線程、更多的操作需要執(zhí)行,那么這個(gè)交錯(cuò)是必然發(fā)生的。
有很多方法來解決線程交錯(cuò)的問題:
- 信號量 Semaphores
- 原子引用 Atomic references
- Monitors
- Condition codes
- Compare and swap
在這篇文章中我們將學(xué)習(xí)如何使用信號量來解決這個(gè)問題。信號量也有很多人稱之為互斥量(Mutex),同一個(gè)時(shí)間只允許一個(gè)線程獲取一個(gè)互斥對象的鎖,通過 Mutex 的簡單屬性就可以用來解決交錯(cuò)的問題。
使用 Mutex 讓計(jì)數(shù)器程序是線程安全的
在 C++11 線程庫中,互斥量包含在 mutex 頭文件中,對應(yīng)的類是 std::mutex,有兩個(gè)重要的方法 mutex:lock() 和 unlock() ,從名字上可得知是用來鎖對象以及釋放鎖對象。一旦某個(gè)互斥量被鎖,那么再次調(diào)用 lock() 返回堵塞值得該對象被釋放。
為了讓我們剛才的計(jì)數(shù)器結(jié)構(gòu)體是線程安全的,我們添加一個(gè) set:mutext 成員,并在每個(gè)方法中通過 lock()/unlock() 方法來進(jìn)行保護(hù):
struct Counter {
std::mutex mutex;
int value;
Counter() : value(0) {}
void increment(){
mutex.lock();
++value;
mutex.unlock();
}
};
然后我們再次測試這個(gè)程序,打印的結(jié)果就是 500 了,而且每次都一樣。
異常和鎖
現(xiàn)在讓我們來看另外一種情況,想象我們的的計(jì)數(shù)器有一個(gè)減操作,并在值為0的時(shí)候拋出異常:
struct Counter {
int value;
Counter() : value(0) {}
void increment(){
++value;
}
void decrement(){
if(value == 0){
throw "Value cannot be less than 0";
}
--value;
}
};
然后我們不需要修改類來訪問這個(gè)結(jié)構(gòu)體,我們創(chuàng)建一個(gè)封裝器:
struct ConcurrentCounter {
std::mutex mutex;
Counter counter;
void increment(){
mutex.lock();
counter.increment();
mutex.unlock();
}
void decrement(){
mutex.lock();
counter.decrement();
mutex.unlock();
}
};
大部分時(shí)候該封裝器運(yùn)行挺好,但是使用 decrement 方法的時(shí)候就會有異常發(fā)生。這是一個(gè)大問題,一旦異常發(fā)生后,unlock 方法就沒被調(diào)用,導(dǎo)致互斥量一直被占用,然后整個(gè)程序就一直處于堵塞狀態(tài)(死鎖),為了解決這個(gè)問題我們需要用 try/catch 結(jié)構(gòu)來處理異常情況:
void decrement(){
mutex.lock();
try {
counter.decrement();
} catch (std::string e){
mutex.unlock();
throw e;
}
mutex.unlock();
}
這個(gè)代碼并不難,但看起來很丑,如果你一個(gè)函數(shù)有 10 個(gè)退出點(diǎn),你就必須為每個(gè)退出點(diǎn)調(diào)用一次 unlock 方法,或許你可能在某個(gè)地方忘掉了 unlock ,那么各種悲劇即將發(fā)生,悲劇發(fā)生將直接導(dǎo)致程序死鎖。
接下來我們看如何解決這個(gè)問題。
自動鎖管理
當(dāng)你需要包含整段的代碼(在我們這里是一個(gè)方法,也可能是一個(gè)循環(huán)體或者其他的控制結(jié)構(gòu)),有這么一種好的解決方法可以避免忘記釋放鎖,那就是 std::lock_guard.
這個(gè)類是一個(gè)簡單的智能鎖管理器,但創(chuàng)建 std::lock_guard 時(shí),會自動調(diào)用互斥量對象的 lock() 方法,當(dāng) lock_guard 析構(gòu)時(shí)會自動釋放鎖,請看下面代碼:
struct ConcurrentSafeCounter {
std::mutex mutex;
Counter counter;
void increment(){
std::lock_guard<std::mutex> guard(mutex);
counter.increment();
}
void decrement(){
std::lock_guard<std::mutex> guar(mutex);
mutex.unlock();
}
};
是不是看起來爽多了?
使用 lock_guard ,你不再需要考慮什么時(shí)候要釋放鎖,這個(gè)工作已經(jīng)由 std::lock_guard 實(shí)例幫你完成。
結(jié)論
在這篇文章中我們學(xué)習(xí)了如何通過信號量/互斥量來保護(hù)共享數(shù)據(jù)。需要記住的是,使用鎖會降低程序性能。在一些高并發(fā)的應(yīng)用環(huán)境中有其他更好的解決辦法,不過這不在本文的討論范疇之內(nèi)。
你可以在 Github 上獲取本文的源碼.
上一篇:C++程序中啟動線程的方法
欄 目:C語言
下一篇:VC++ 獲取系統(tǒng)時(shí)間的方法匯總
本文標(biāo)題:探究在C++程序并發(fā)時(shí)保護(hù)共享數(shù)據(jù)的問題
本文地址:http://www.jygsgssxh.com/a1/Cyuyan/2976.html
您可能感興趣的文章
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言沒有round函數(shù) round c語言
- 04-02C語言中怎么打出三角函數(shù) c語言中怎么打出三角函數(shù)的值
- 01-10深入理解C++中常見的關(guān)鍵字含義
- 01-10使用C++實(shí)現(xiàn)全排列算法的方法詳解
- 01-10APUE筆記之:進(jìn)程環(huán)境詳解
- 01-10c++中inline的用法分析
- 01-10用C++實(shí)現(xiàn)DBSCAN聚類算法
- 01-10全排列算法的非遞歸實(shí)現(xiàn)與遞歸實(shí)現(xiàn)的方法(C++)
- 01-10C++大數(shù)模板(推薦)


閱讀排行
本欄相關(guān)
- 04-02c語言函數(shù)調(diào)用后清空內(nèi)存 c語言調(diào)用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調(diào)用函數(shù)求fibo C語言調(diào)用函數(shù)求
隨機(jī)閱讀
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 04-02jquery與jsp,用jquery
- 08-05織夢dedecms什么時(shí)候用欄目交叉功能?
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10delphi制作wav文件的方法


