併發**中最常見的錯誤之一就是競爭條件(race condition)。而其中最常見的就是資料競爭(data race),從整體上來看,所有執行緒之間共享資料的問題,都是修改資料導致的,如果所有的共享資料都是唯讀的,就不會發生問題。但是這是不可能的,大部分共享資料都是要被修改的。
而c++
中常見的cout
就是乙個共享資源,如果在多個執行緒同時執行cout,你會發發現很奇怪的問題:
#include
#include
#include
using namespace std;
// 普通函式 無參
void
function_1()
intmain()
你有很大的機率發現列印會出現類似於from t1: from main: 64
這樣奇怪的列印結果。cout
是基於流的,會先將你要列印的內容放入緩衝區,可能剛剛乙個執行緒剛剛放入from t1:
,另乙個執行緒就執行了,導致輸出變亂。而c
語言中的printf
不會發生這個問題。
解決辦法就是要對cout
這個共享資源進行保護。在c++
中,可以使用互斥鎖std::mutex
進行資源保護,標頭檔案是#include
,共有兩種操作:鎖定(lock)與解鎖(unlock)。將cout
重新封裝成乙個執行緒安全的函式:
#include
#include
#include
#include
using namespace std;
std:
:mutex mu;
// 使用鎖保護
void
shared_print
(string msg,
int id)
void
function_1()
intmain()
修改完之後,執行可以發現列印沒有問題了。但是還有乙個隱藏著的問題,如果mu.lock()
和mu.unlock()
之間的語句發生了異常,會發生什麼?unlock()
語句沒有機會執行!導致導致mu
一直處於鎖著的狀態,其他使用shared_print()
函式的執行緒就會阻塞。
解決這個問題也很簡單,使用c++
中常見的raii
技術,即獲取資源即初始化(resource acquisition is initialization)技術,這是c++
中管理資源的常用方式。簡單的說就是在類的建構函式中建立資源,在析構函式中釋放資源,因為就算發生了異常,c++
也能保證類的析構函式能夠執行。我們不需要自己寫個類包裝mutex
,c++
庫已經提供了std::lock_guard
類模板,使用方法如下:
void
shared_print
(string msg,
int id)
可以實現自己的std::lock_guard
,類似這樣:
class mutexlockguard
~mutexlockguard()
private:
std:
:mutex& mutex_;
};
上面的std::mutex
互斥元是個全域性變數,他是為shared_print()
準備的,這個時候,我們最好將他們繫結在一起,比如說,可以封裝成乙個類。由於cout
是個全域性共享的變數,沒法完全封裝,就算你封裝了,外面還是能夠使用cout
,並且不用通過鎖。下面使用檔案流舉例:
#include
#include
#include
#include
#include
using namespace std;
std:
:mutex mu;
class logfile
~logfile()
void
shared_print
(string msg,
int id)};
void
function_1
(logfile& log)
intmain()
上面的logfile
類封裝了乙個mutex
和乙個ofstream
物件,然後shared_print
函式在mutex
的保護下,是執行緒安全的。使用的時候,先定義乙個logfile
的例項log
,主線程中直接使用,子執行緒中通過引用傳遞過去(也可以使用單例來實現),這樣就能保證資源被互斥鎖保護著,外面沒辦法使用但是使用資源。
但是這個時候還是得小心了!用互斥元保護資料並不只是像上面那樣保護每個函式,就能夠完全的保證執行緒安全,如果將資源的指標或者引用不小心傳遞出來了,所有的保護都白費了!要記住一下兩點:
不要提供函式讓使用者獲取資源。
std:
:mutex mu;
class logfile
~logfile()
void
shared_print
(string msg,
int id)
// never return f to the outside world
ofstream&
getstream()
};
不要資源傳遞給使用者的函式。
class logfile
~logfile()
void
shared_print
(string msg,
int id)
// never return f to the outside world
ofstream&
getstream()
// never pass f as an argument to user provided function
void
process
(void
fun(ostream&))
};
以上兩種做法都會將資源暴露給使用者,造成不必要的安全隱患。
stl
中的stack
類是執行緒不安全的,於是你模仿著想寫乙個屬於自己的執行緒安全的類stack
。於是,你在push
和pop
等操作得時候,加了互斥鎖保護資料。但是在多執行緒環境下使用使用你的stack
類的時候,卻仍然有可能是執行緒不安全的,why?
假設你的stack
類的介面如下:
class stack
void
pop();
//彈出棧頂元素
int&
top();
//獲取棧頂元素
void
push
(int x)
;//將元素放入棧
private:
vector<
int> data;
std:
:mutex _mu;
//保護內部資料
};
類中的每乙個函式都是執行緒安全的,但是組合起來卻不是。加入棧中有9,3,8,6
共4個元素,你想使用兩個執行緒分別取出棧中的元素進行處理,如下所示:
thread a thread b
int v = st.
top();
// 6
int v = st.
top();
// 6
st.pop()
;//彈出6
st.pop();
//彈出8
process
(v);
//處理6
process
(v);
//處理6
可以發現在這種執行順序下, 棧頂元素被處理了兩遍,而且多彈出了乙個元素8
,導致8
沒有被處理!這就是由於介面設計不當引起的競爭。解決辦法就是將這兩個介面合併為乙個介面!就可以得到執行緒安全的棧。
class stack
int&
pop();
//彈出棧頂元素並返回
void
push
(int x)
;//將元素放入棧
private:
vector<
int> data;
std:
:mutex _mu;
//保護內部資料};
//下面這樣使用就不會發生問題
int v = st.
pop();
// 6
process
(v);
但是注意:這樣修改之後是執行緒安全的,但是並不是異常安全的,這也是為什麼stl
中棧的出棧操作分解成了兩個步驟的原因。(為什麼不是異常安全的還沒想明白。。)
所以,為了保護共享資料,還得好好設計介面才行。
C 11多執行緒程式設計
1 c 11新標準引入了五個標頭檔案支援多執行緒程式設計,分別如下 該標頭檔案 該標頭檔案主要宣告了std thread類,其中std this thread 提供了一些輔助函式 命名空間也在該標頭檔案中 該標頭檔案主要宣告了std atomic和std atomic flag兩個類,另外還宣告了一...
C 11的多執行緒併發程式設計(三)
疫情確診加疑似都快7w 了,能做的也只能在家大門不出,二門不邁了,寫一篇繼續記錄c 11的多執行緒併發程式設計 在建立的子執行緒中,呼叫類的物件時考慮到在主線程定義引數執行帶參建構函式時,子執行緒detach時,主線程線執行完清除變數,那麼一般在建立主線程時直接構造物件,並在輸入函式出增加引用。這樣...
c 11 多執行緒程式設計 原子
以下是我關於c 11多執行緒程式設計的學習體會,希望大家多指正 目的 1 原子型別的引入意味著不需要額外的同步機制就可以執行併發的讀寫操作。2 原子操作的確可以作為解決共享資料引起的問題的一種有效的手段。示例 已在vs2015 編譯通過 test atomic 1.cpp 定義控制台應用程式的入口點...