多線程間的共用資料如果不加以約束是有問題的。最簡單的方法就是對資料結構採用某種保護機制,通俗的表達就是:
確保只有進行修改的線程才能看到不變數被破壞時的中間狀態。從其他訪問線程的角度來看,修改不是已經完成了,就是還沒開始。
1.使用互斥量保護共用資料
當訪問共用資料前,使用互斥量將相關資料鎖住,再當訪問結束後,再將資料解鎖。C++標準庫為互斥量提供了一個RAII文法的模板類std::lack_guard ,其會在構造的時候提供已鎖的互斥量,並在析構的時候進行解鎖.
#include <list>#include <mutex>#include <algorithm>std::list<int> some_list; // 1std::mutex some_mutex; // 2void add_to_list(int new_value){ std::lock_guard<std::mutex> guard(some_mutex); // 3 some_list.push_back(new_value);}bool list_contains(int value_to_find){ std::lock_guard<std::mutex> guard(some_mutex); // 4 return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();}
上述例子通過lock_guart<std::mutex>使得add_to_list和list_contains這兩個函數對資料的訪問是互斥的。從而保證多線程中資料的安全。
2.堤防介面內在的條件競爭
比如你在寫一個棧的資料結構,並且給棧的push\pop\top\empty\size\等介面增加了互斥保護。看以下代碼:
stack<int> s;if (! s.empty()){ // 1int const value = s.top(); // 2s.pop(); // 3do_something(value);}
以上只是單安全執行緒,對於多線程在1和2之間,可能有來自另一個線程的pop()調用並刪除了最後一個元素,此時就會出問題。
因為鎖的粒度太小,需要保護的操作並未全覆蓋到。可以考慮適當增大鎖的粒度。
std::shared_ptr<T> pop(){ std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); // 在調用pop前,檢查棧是否為空白 std::shared_ptr<T> const res(std::make_shared<T>(data.top())); // 在修改堆棧前,分配出傳回值 data.pop();return res;}void pop(T& value){ std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); value=data.top(); data.pop();}
3.堤防死結問題
造成死結最大的問題是:由兩個或兩個以上的互斥量來鎖定一個操作。一對線程需要對他們所有的互斥量做一些操作,其中每個線程都有一個互斥量,且等待另一個解鎖。這樣沒有線程能工作,因為他們都在等待對方釋放互斥量。
避免死結的一般建議,就是讓兩個互斥量總以相同的順序上鎖:總在互斥量B之前鎖住互斥量A,可以有效防止大部分問題。一個更好的方法是利用C++提供的std::lock,可以一次性鎖住多個互斥量。看以下例子:
class some_big_object;void swap(some_big_object& lhs,some_big_object& rhs);class X{private: some_big_object some_detail; std::mutex m;public: X(some_big_object const& sd):some_detail(sd){} friend void swap(X& lhs, X& rhs) { if(&lhs==&rhs) return; std::lock(lhs.m,rhs.m); // 1 std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // 2 std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); // 3 swap(lhs.some_detail,rhs.some_detail); }};
調用std::lock() ①鎖住兩個互斥量,並且兩個std:lock_guard 執行個體已經建立好②③,還有一個互斥量。提供std::adopt_lock 參數除了表示std::lock_guard 對象已經上鎖外,還表示現成的鎖,而非嘗試建立新的鎖。
注意:一個互斥量可以在同一線程上多次上鎖(std::recursive_mutex)。
以下是設計鎖的幾條忠告:
1.盡量避免嵌套鎖,一個線程已獲得鎖時別再擷取第二個
2.避免在持有鎖是調用使用者提供的代碼,因為使用者的代碼不可預知,有發生死結的可能
3.使用固定順序擷取鎖--多於多個鎖時可有效避免死結
對於鎖的粒度問題也是需要注意的,大的粒度可以保護更多的資料,但是其對效能的影響也越大。同時需要儘可能他將鎖的持有時間減到最小。
比較操作符一次鎖住一個互斥量:
friend bool operator==(Y const& lhs, Y const& rhs){
if(&lhs==&rhs) return true; int const lhs_value=lhs.get_detail(); // 2 int const rhs_value=rhs.get_detail(); // 3 return lhs_value==rhs_value; // 4}
利用int的拷貝來減少鎖的等待時間,是一種高效的做法。