競(jìng)爭(zhēng)條件
并發(fā)代碼中最常見的錯(cuò)誤之一就是競(jìng)爭(zhēng)條件(race condition)育特。而其中最常見的就是數(shù)據(jù)競(jìng)爭(zhēng)(data race)丙号,從整體上來看,所有線程之間共享數(shù)據(jù)的問題缰冤,都是修改數(shù)據(jù)導(dǎo)致的犬缨,如果所有的共享數(shù)據(jù)都是只讀的,就不會(huì)發(fā)生問題棉浸。但是這是不可能的怀薛,大部分共享數(shù)據(jù)都是要被修改的。
而c++
中常見的cout
就是一個(gè)共享資源迷郑,如果在多個(gè)線程同時(shí)執(zhí)行cout
枝恋,你會(huì)發(fā)發(fā)現(xiàn)很奇怪的問題:
#include <iostream>
#include <thread>
#include <string>
using namespace std;
// 普通函數(shù) 無參
void function_1() {
for(int i=0; i>-100; i--)
cout << "From t1: " << i << endl;
}
int main()
{
std::thread t1(function_1);
for(int i=0; i<100; i++)
cout << "From main: " << i << endl;
t1.join();
return 0;
}
你有很大的幾率發(fā)現(xiàn)打印會(huì)出現(xiàn)類似于From t1: From main: 64
這樣奇怪的打印結(jié)果。cout
是基于流的嗡害,會(huì)先將你要打印的內(nèi)容放入緩沖區(qū)焚碌,可能剛剛一個(gè)線程剛剛放入From t1:
,另一個(gè)線程就執(zhí)行了霸妹,導(dǎo)致輸出變亂十电。而c
語言中的printf
不會(huì)發(fā)生這個(gè)問題。
使用互斥元保護(hù)共享數(shù)據(jù)
解決辦法就是要對(duì)cout
這個(gè)共享資源進(jìn)行保護(hù)抑堡。在c++
中摆出,可以使用互斥鎖std::mutex
進(jìn)行資源保護(hù)朗徊,頭文件是#include <mutex>
首妖,共有兩種操作:鎖定(lock)與解鎖(unlock)。將cout
重新封裝成一個(gè)線程安全的函數(shù):
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
std::mutex mu;
// 使用鎖保護(hù)
void shared_print(string msg, int id) {
mu.lock(); // 上鎖
cout << msg << id << endl;
mu.unlock(); // 解鎖
}
void function_1() {
for(int i=0; i>-100; i--)
shared_print(string("From t1: "), i);
}
int main()
{
std::thread t1(function_1);
for(int i=0; i<100; i++)
shared_print(string("From main: "), i);
t1.join();
return 0;
}
修改完之后爷恳,運(yùn)行可以發(fā)現(xiàn)打印沒有問題了有缆。但是還有一個(gè)隱藏著的問題,如果mu.lock()
和mu.unlock()
之間的語句發(fā)生了異常,會(huì)發(fā)生什么棚壁?unlock()
語句沒有機(jī)會(huì)執(zhí)行杯矩!導(dǎo)致導(dǎo)致mu
一直處于鎖著的狀態(tài),其他使用shared_print()
函數(shù)的線程就會(huì)阻塞袖外。
解決這個(gè)問題也很簡(jiǎn)單史隆,使用c++
中常見的RAII
技術(shù),即獲取資源即初始化(Resource Acquisition Is Initialization)技術(shù)曼验,這是c++
中管理資源的常用方式泌射。簡(jiǎn)單的說就是在類的構(gòu)造函數(shù)中創(chuàng)建資源,在析構(gòu)函數(shù)中釋放資源鬓照,因?yàn)榫退惆l(fā)生了異常熔酷,c++
也能保證類的析構(gòu)函數(shù)能夠執(zhí)行。我們不需要自己寫個(gè)類包裝mutex
豺裆,c++
庫已經(jīng)提供了std::lock_guard
類模板拒秘,使用方法如下:
void shared_print(string msg, int id) {
//構(gòu)造的時(shí)候幫忙上鎖,析構(gòu)的時(shí)候釋放鎖
std::lock_guard<std::mutex> guard(mu);
//mu.lock(); // 上鎖
cout << msg << id << endl;
//mu.unlock(); // 解鎖
}
可以實(shí)現(xiàn)自己的std::lock_guard
臭猜,類似這樣:
class MutexLockGuard
{
public:
explicit MutexLockGuard(std::mutex& mutex)
: mutex_(mutex)
{
mutex_.lock();
}
~MutexLockGuard()
{
mutex_.unlock();
}
private:
std::mutex& mutex_;
};
為保護(hù)共享數(shù)據(jù)精心組織代碼
上面的std::mutex
互斥元是個(gè)全局變量躺酒,他是為shared_print()
準(zhǔn)備的,這個(gè)時(shí)候获讳,我們最好將他們綁定在一起阴颖,比如說,可以封裝成一個(gè)類丐膝。由于cout
是個(gè)全局共享的變量量愧,沒法完全封裝,就算你封裝了帅矗,外面還是能夠使用cout
偎肃,并且不用通過鎖。下面使用文件流舉例:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
std::mutex mu;
class LogFile {
std::mutex m_mutex;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(mu);
f << msg << id << endl;
}
};
void function_1(LogFile& log) {
for(int i=0; i>-100; i--)
log.shared_print(string("From t1: "), i);
}
int main()
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i=0; i<100; i++)
log.shared_print(string("From main: "), i);
t1.join();
return 0;
}
上面的LogFile
類封裝了一個(gè)mutex
和一個(gè)ofstream
對(duì)象浑此,然后shared_print
函數(shù)在mutex
的保護(hù)下累颂,是線程安全的。使用的時(shí)候凛俱,先定義一個(gè)LogFile
的實(shí)例log
紊馏,主線程中直接使用,子線程中通過引用傳遞過去(也可以使用單例來實(shí)現(xiàn)),這樣就能保證資源被互斥鎖保護(hù)著蒲犬,外面沒辦法使用但是使用資源朱监。
但是這個(gè)時(shí)候還是得小心了!用互斥元保護(hù)數(shù)據(jù)并不只是像上面那樣保護(hù)每個(gè)函數(shù)原叮,就能夠完全的保證線程安全赫编,如果將資源的指針或者引用不小心傳遞出來了巡蘸,所有的保護(hù)都白費(fèi)了!要記住一下兩點(diǎn):
-
不要提供函數(shù)讓用戶獲取資源擂送。
std::mutex mu; class LogFile { std::mutex m_mutex; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::lock_guard<std::mutex> guard(mu); f << msg << id << endl; } // Never return f to the outside world ofstream& getStream() { return f; //never do this !!! } };
-
不要資源傳遞給用戶的函數(shù)悦荒。
class LogFile { std::mutex m_mutex; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::lock_guard<std::mutex> guard(mu); f << msg << id << endl; } // Never return f to the outside world ofstream& getStream() { return f; //never do this !!! } // Never pass f as an argument to user provided function void process(void fun(ostream&)) { fun(f); } };
以上兩種做法都會(huì)將資源暴露給用戶,造成不必要的安全隱患嘹吨。
接口設(shè)計(jì)中也存在競(jìng)爭(zhēng)條件
STL
中的stack
類是線程不安全的搬味,于是你模仿著想寫一個(gè)屬于自己的線程安全的類Stack
。于是蟀拷,你在push
和pop
等操作得時(shí)候身腻,加了互斥鎖保護(hù)數(shù)據(jù)。但是在多線程環(huán)境下使用使用你的Stack
類的時(shí)候匹厘,卻仍然有可能是線程不安全的嘀趟,why?
假設(shè)你的Stack
類的接口如下:
class Stack
{
public:
Stack() {}
void pop(); //彈出棧頂元素
int& top(); //獲取棧頂元素
void push(int x);//將元素放入棧
private:
vector<int> data;
std::mutex _mu; //保護(hù)內(nèi)部數(shù)據(jù)
};
類中的每一個(gè)函數(shù)都是線程安全的愈诚,但是組合起來卻不是她按。加入棧中有9,3,8,6
共4個(gè)元素,你想使用兩個(gè)線程分別取出棧中的元素進(jìn)行處理炕柔,如下所示:
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
可以發(fā)現(xiàn)在這種執(zhí)行順序下酌泰, 棧頂元素被處理了兩遍,而且多彈出了一個(gè)元素8
匕累,導(dǎo)致`8沒有被處理陵刹!這就是由于接口設(shè)計(jì)不當(dāng)引起的競(jìng)爭(zhēng)。解決辦法就是將這兩個(gè)接口合并為一個(gè)接口欢嘿!就可以得到線程安全的棧衰琐。
class Stack
{
public:
Stack() {}
int& pop(); //彈出棧頂元素并返回
void push(int x);//將元素放入棧
private:
vector<int> data;
std::mutex _mu; //保護(hù)內(nèi)部數(shù)據(jù)
};
//下面這樣使用就不會(huì)發(fā)生問題
int v = st.pop(); // 6
process(v);
但是注意:這樣修改之后是線程安全的,但是并不是異常安全的炼蹦,這也是為什么STL
中棧的出棧操作分解成了兩個(gè)步驟的原因羡宙。(為什么不是異常安全的還沒想明白。掐隐。)
所以狗热,為了保護(hù)共享數(shù)據(jù),還得好好設(shè)計(jì)接口才行虑省。