共享數(shù)據(jù)帶來的問題
條件競(jìng)爭(zhēng)
條件競(jìng)爭(zhēng)產(chǎn)生
并發(fā)中競(jìng)爭(zhēng)條件的形成,取決于一個(gè)以上線程的相對(duì)執(zhí)行順序,每個(gè)線程都搶著完成自己的任務(wù)滞造。大多數(shù)情況下路召,即使改變執(zhí)行順序勃刨,也是良性競(jìng)爭(zhēng)波材,結(jié)果是可以接受的。例如有兩個(gè)線程同時(shí)向一個(gè)處理隊(duì)列中添加任務(wù)身隐,由于系統(tǒng)提供的不變量保持不變廷区,所以先后順序并不會(huì)造成影響。不變量遭到破壞以后贾铝,才會(huì)產(chǎn)生條件競(jìng)爭(zhēng)躲因,這種競(jìng)爭(zhēng)方式通常表示為惡性條件競(jìng)爭(zhēng)。
惡性條件競(jìng)爭(zhēng)通常發(fā)生于對(duì)多于一個(gè)的數(shù)據(jù)塊的修改的時(shí)候忌傻,操作訪問兩個(gè)獨(dú)立的數(shù)據(jù)塊大脉,獨(dú)立的指令將會(huì)對(duì)數(shù)據(jù)塊進(jìn)行修改,并且可能一個(gè)線程正在進(jìn)行時(shí)水孩,另一個(gè)線程就對(duì)數(shù)據(jù)塊進(jìn)行了訪問镰矿。因?yàn)槌霈F(xiàn)的概率太小,條件競(jìng)爭(zhēng)很難查找俘种,并且也很難進(jìn)行浮現(xiàn)秤标。當(dāng)系統(tǒng)負(fù)載增加的時(shí)候,隨著執(zhí)行數(shù)量的增加宙刘,執(zhí)行序列的問題復(fù)現(xiàn)的概率也在增加苍姜,但是這只會(huì)出現(xiàn)在負(fù)載比較大的時(shí)候。條件競(jìng)爭(zhēng)通常是時(shí)間敏感的悬包,所以程序以調(diào)試模式進(jìn)行運(yùn)行的時(shí)候衙猪,他們將會(huì)完全消失。
避免惡性條件競(jìng)爭(zhēng)
- 對(duì)數(shù)據(jù)結(jié)構(gòu)采用某種保護(hù)機(jī)制布近,確保只有進(jìn)行修改的線程才能看到不變量被破壞的中間狀態(tài)垫释。從其他的線程來看,修改不是已經(jīng)完成的撑瞧,就是還沒有開始棵譬。
- 對(duì)數(shù)據(jù)結(jié)構(gòu)和不變量的設(shè)計(jì)進(jìn)行修改,修改完成的結(jié)構(gòu)必須可以完成一系列不可分割的變化预伺,也就是保證每個(gè)不變量保持穩(wěn)定的狀態(tài)订咸,即所謂的無鎖編程。
- 使用事物的方式進(jìn)行數(shù)據(jù)結(jié)構(gòu)的更新酬诀。當(dāng)所需的一些數(shù)據(jù)和讀取都存儲(chǔ)在事務(wù)日志中脏嚷,然后將之前的操作合并為一步再進(jìn)行提交。當(dāng)數(shù)據(jù)結(jié)構(gòu)被另一個(gè)線程修改之后料滥,或者處理已經(jīng)重啟的情況下然眼,提交就會(huì)無法進(jìn)行。這被稱作“軟件事物內(nèi)存”
使用互斥量保護(hù)數(shù)據(jù)
使用互斥量保護(hù)共享數(shù)據(jù)
當(dāng)訪問共享數(shù)據(jù)的前葵腹,使用互斥量將相關(guān)的數(shù)據(jù)鎖住高每,當(dāng)訪問結(jié)束之后屿岂,再將數(shù)據(jù)進(jìn)行解鎖。當(dāng)一個(gè)線程使用特定互斥量鎖住共享數(shù)據(jù)時(shí)候鲸匿,其他的線程想要訪問鎖住的數(shù)據(jù)爷怀,都必須等到之前的那個(gè)線程對(duì)數(shù)據(jù)進(jìn)行解鎖后,才能進(jìn)行訪問带欢。這就保證了所有線程都可以看到共享數(shù)據(jù)运授,而不破壞不變量。
C++中使用互斥量
C++中通過實(shí)例化std::mutex創(chuàng)建互斥量乔煞,通過調(diào)用成員函數(shù)lock()進(jìn)行上鎖吁朦,unlock()進(jìn)行解鎖。C++標(biāo)準(zhǔn)庫為互斥量提供了一個(gè)RAII語法的模板類std::lack_guard渡贾,其會(huì)在構(gòu)造的時(shí)候提供已經(jīng)上鎖的互斥量逗宜,并且在析構(gòu)的時(shí)候進(jìn)行解鎖,從而保證了一個(gè)已經(jīng)上鎖的互斥量都會(huì)被正確的解鎖空骚。
#include<mutex>
#include<list>
#include<algorithm>
using namespace std;
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value){
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
void list_contains(int value_to_find){
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(),some_list.end(),value_to_find)!=some_list.end();
}
在某些情況下纺讲,使用全局變量沒有問題,但是在大多數(shù)的情況下囤屹,互斥量通常會(huì)和保護(hù)的數(shù)據(jù)放在同一個(gè)類中熬甚,而不是定義成全局變量。
將其放在同一個(gè)類中肋坚,就可以將其聯(lián)系在一起乡括,也可以對(duì)類的功能進(jìn)行封裝,并且進(jìn)行數(shù)據(jù)的保護(hù)冲簿。在這種情況下粟判,函數(shù)add_to_list和list_contains都可以作為這個(gè)類的成員函數(shù)。 互斥量和要保護(hù)的數(shù)據(jù)峦剔,在類中都要定義為private成員。當(dāng)所有成員函數(shù)都會(huì)在調(diào)用的時(shí)候?qū)?shù)據(jù)進(jìn)行上鎖角钩,結(jié)束的時(shí)候?qū)?shù)據(jù)進(jìn)行解鎖吝沫,就可以保證數(shù)據(jù)訪問的時(shí)候,不變量不會(huì)被破壞递礼。
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class some_data{
int a;
string b;
public:
some_data():a(10),b("This is a test"){};
void do_something(){
cout<<b<<"\n";
cout<<a<<"\n";
};
void reset_a(int x){
a=x;
}
void reset_b(string s){
b=s;
}
};
//構(gòu)造一個(gè)data_wrapper 類對(duì)data數(shù)據(jù)結(jié)構(gòu)加以保護(hù)
//同時(shí)可以使用參數(shù)傳遞的方式 傳入處理函數(shù)
//這里只使用了一個(gè)互斥量 保護(hù)的是data整體
class data_wrapper{
private:
some_data data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func){
std::lock_guard<std::mutex> l(m);
func(data);
}
};
void malicious_function(some_data& protected_data){
protected_data.do_something();
protected_data.reset_a(10);
protected_data.reset_b("another test");
protected_data.do_something();
}
int main(){
data_wrapper x;
x.process_data(malicious_function);
return 0;
}
當(dāng)其中的一個(gè)成員函數(shù)返回的是保護(hù)數(shù)據(jù)的指針或者引用的時(shí)候惨险,會(huì)破壞對(duì)數(shù)據(jù)的保護(hù)。
精心組織代碼來保護(hù)共享數(shù)據(jù)
使用互斥量保護(hù)數(shù)據(jù)脊髓,并不是僅僅在每一個(gè)成員函數(shù)中都加入一個(gè)std::lock_guard對(duì)象那么簡(jiǎn)單辫愉。一個(gè)迷失的指針或者引用,會(huì)將這種保護(hù)形同虛設(shè)将硝。只要沒有成員函數(shù)通過返回值或者是輸出參數(shù)的形式向其調(diào)用者返回指向受保護(hù)數(shù)據(jù)的指針或者引用恭朗,數(shù)據(jù)就是安全的屏镊。
在確保成員函數(shù)不會(huì)傳出指針或者引用的同時(shí),檢查成員函數(shù)是否通過指針或者引用的方式來調(diào)用也是重要的痰腮。函數(shù)可能沒在互斥量保護(hù)的區(qū)域內(nèi)而芥,存儲(chǔ)著指針或者引用,這樣就是危險(xiǎn)的膀值。更為危險(xiǎn)的是棍丐,將保護(hù)數(shù)據(jù)作為一個(gè)運(yùn)行時(shí)參數(shù)。
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class some_data{
int a;
string b;
public:
some_data():a(10),b("This is a test"){};
void do_something(){
cout<<b<<"\n";
cout<<a<<"\n";
};
void reset_a(int x){
a=x;
}
void reset_b(string s){
b=s;
}
};
//構(gòu)造一個(gè)data_wrapper 類對(duì)data數(shù)據(jù)結(jié)構(gòu)加以保護(hù)
//同時(shí)可以使用參數(shù)傳遞的方式 傳入處理函數(shù)
//這里只使用了一個(gè)互斥量 保護(hù)的是data整體
class data_wrapper{
private:
some_data data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func){
std::lock_guard<std::mutex> l(m);
func(data);
}
};
//這里使用了一個(gè)不受保護(hù)的指針 導(dǎo)致 在沒有互斥量的情況下 也可以進(jìn)行對(duì)函數(shù)的訪問和改變
//是錯(cuò)誤的需要進(jìn)行避免的操作
some_data * unprotected_data;
void malicious_function(some_data& protected_data){
protected_data.do_something();
protected_data.reset_a(10);
protected_data.reset_b("another test");
protected_data.do_something();
unprotected_data=&protected_data;
}
int main(){
data_wrapper x;
x.process_data(malicious_function);
unprotected_data.do_something();
return 0;
}
使用接口內(nèi)在的競(jìng)爭(zhēng)條件
在使用了互斥量或者是其他機(jī)制保護(hù)共享數(shù)據(jù)的時(shí)候沧踏,也需要避免接口內(nèi)部的條件競(jìng)爭(zhēng)歌逢。
以雙向鏈表為例,為了可以線程安全的刪除一個(gè)節(jié)點(diǎn)翘狱,需要確保防止對(duì)三個(gè)節(jié)點(diǎn)(待刪除節(jié)點(diǎn)以及前后相鄰的節(jié)點(diǎn))的并發(fā)訪問趋翻。如果只是對(duì)指向每個(gè)節(jié)點(diǎn)的指針進(jìn)行了訪問保護(hù),就和沒有使用互斥量是一樣的盒蟆,條件競(jìng)爭(zhēng)仍然是存在的踏烙。除了指針,整個(gè)數(shù)據(jù)結(jié)構(gòu)和整個(gè)刪除操作都是需要進(jìn)行保護(hù)的历等。這種情況下最簡(jiǎn)單的解決方案就是使用互斥量保護(hù)整個(gè)鏈表讨惩。
構(gòu)建一個(gè)類似于std::stack結(jié)構(gòu)的棧,除了構(gòu)造函數(shù)和swap()操作之外寒屯,還需要提供push(),pop(),top()等操作荐捻。