線程間共享數(shù)據(jù)的問題
多線程之間共享數(shù)據(jù),最大的問題便是數(shù)據(jù)競(jìng)爭(zhēng)導(dǎo)致的異常問題。多個(gè)線程操作同一塊資源,如果不做任何限制,那么一定會(huì)發(fā)生錯(cuò)誤。例如:
1 int g_nResource = 0; 2 void thread_entry() 3 { 4 for (int i = 0; i < 10000000; ++i) 5 g_nResource++; 6 } 7 8 int main() 9 { 10 thread th1(thread_entry); 11 thread th2(thread_entry); 12 th1.join(); 13 th2.join(); 14 cout << g_nResource << endl; 15 return 0; 16 }
輸出:
10161838
顯然,上面的輸出結(jié)果存在問題。出現(xiàn)錯(cuò)誤的原因可能是:
某一時(shí)刻,th1線程獲得CPU時(shí)間片,將g_nResource從100增加至200后時(shí)間片結(jié)束,保存上下文并切換至th2線程。th2將g_nResource增加至300,結(jié)束時(shí)間片,保存上下文并切換回th1線程。此時(shí),還原上下文,g_nResource會(huì)還原成之前保存的200的值。
在并發(fā)編程中,操作由兩個(gè)或多個(gè)線程負(fù)責(zé),它們爭(zhēng)先恐后執(zhí)行各自的操作,而結(jié)果取決于它們執(zhí)行的相對(duì)次序,每一種次序都是條件競(jìng)爭(zhēng)。很多時(shí)候,這是良性行為,因?yàn)槿靠赡艿慕Y(jié)果都可以接受,即便線程變換了相對(duì)次序。例如,往容器中添加數(shù)據(jù)項(xiàng),不管怎么添加,只要容器的容量夠,總能將所有數(shù)據(jù)項(xiàng)填入,我們只關(guān)心是否能全部放入,對(duì)于元素的次序并不care。
真正讓人煩惱的,是惡性條件競(jìng)爭(zhēng)。要完成一項(xiàng)操作,需要對(duì)共享資源進(jìn)行修改,當(dāng)其中一個(gè)線程還未完成數(shù)據(jù)寫入時(shí),另一個(gè)線程不期而訪。惡性條件競(jìng)爭(zhēng)會(huì)產(chǎn)生未定義的行為,并且每次產(chǎn)生的結(jié)果都不相同,無形中增加故障排除的難度。
歸根結(jié)底,多線程共享數(shù)據(jù)的問題大多數(shù)都由線程對(duì)數(shù)據(jù)的修改引發(fā)的。如果所有共享數(shù)據(jù)都是只讀數(shù)據(jù),就不會(huì)有問題。因?yàn)椋魯?shù)據(jù)被某個(gè)線程讀取,無論是否存在其他線程也在讀取,該數(shù)據(jù)都不會(huì)受到影響。然而,如果多個(gè)線程共享數(shù)據(jù),只要一個(gè)線程開始改動(dòng)數(shù)據(jù),就會(huì)帶來很多隱患,產(chǎn)生麻煩。解決辦法就是使用互斥對(duì)數(shù)據(jù)進(jìn)行保護(hù)。
1 int g_nResource = 0; 2 std::mutex _mutex; //使用互斥 3 void thread_entry() 4 { 5 _mutex.lock(); //加鎖 6 for (int i = 0; i < 10000000; ++i) 7 g_nResource++; 8 _mutex.unlock(); //解鎖 9 }
?輸出:
20000000
用互斥保護(hù)共享數(shù)據(jù)
為了達(dá)到我們想要效果,C++11引入了互斥(mutual exclusion)。互斥是一把對(duì)資源的鎖,線程訪問資源時(shí),先鎖住與該資源相關(guān)的互斥,若其他線程試圖再給它加鎖,則須等待,直至最初成功加鎖的線程把該互斥解鎖。這確保了全部線程所見到的共享數(shù)據(jù)是自洽的(self-consistent),不變量沒有被破壞。
在C++中使用互斥
std::mutex
std::mutex是c++中最基本的互斥量。該類定義在<mutex>頭文件中。
構(gòu)造函數(shù)
1 mutex(); 2 3 //不支持拷貝構(gòu)造,也不支持移動(dòng)構(gòu)造(有定義拷貝,則無移動(dòng)) 4 mutex(const mutex&) = delete; 5 mutex& operator=(const mutex&) = delete;
剛初始化的互斥處于unlocked狀態(tài)。
lock()函數(shù)
1 void lock();
用于鎖住該互斥量,有如下3中情況:
- 當(dāng)前沒有被鎖,則當(dāng)前線程鎖住互斥量,在未調(diào)用unlock()函數(shù)前,線程擁有該鎖。
- 被其他線程鎖住,則當(dāng)前線程被阻塞,一直等待其他線程釋放鎖。
- 被當(dāng)前線程鎖住,再次加鎖會(huì)產(chǎn)生異常。
unlock()函數(shù)
1 void unlock();
解鎖,當(dāng)前線程釋放對(duì)互斥量的所有權(quán)。在無鎖情況下調(diào)用unlock()函數(shù),將導(dǎo)致異常。
try_lock()函數(shù)
bool try_lock();
嘗試鎖住互斥量,如果互斥量被其他線程占用,該函數(shù)會(huì)返回false,并不會(huì)阻塞線程。有如下3中情況:
- 當(dāng)前沒有被鎖,則當(dāng)前線程鎖住互斥量,并返回true,在未調(diào)用unlock函數(shù)前,該線程擁有該鎖。
- 被其他線程鎖住,該函數(shù)返回false,線程并不會(huì)被阻塞。
- 被當(dāng)前線程鎖住,再次嘗試獲取鎖,返回false。
案例
1 int g_nResource = 0; 2 std::mutex _mutex; 3 void thread_entry() 4 { 5 while (1) 6 { 7 if (_mutex.try_lock()) 8 { 9 cout << this_thread::get_id() << " get lock\n"; 10 for (int i = 0; i < 10000000; ++i) 11 g_nResource++; 12 _mutex.unlock(); 13 return; 14 } 15 else 16 { 17 cout << this_thread::get_id() << " no get lock\n"; 18 this_thread::sleep_for(std::chrono::milliseconds(500)); 19 } 20 } 21 } 22 23 int main() 24 { 25 thread th1(thread_entry); 26 thread th2(thread_entry); 27 th1.join(); 28 th2.join(); 29 cout << "Result = " << g_nResource << endl; 30 }
輸出:
131988 get lock 136260 no get lock 136260 get lock Result = 20000000
上面代碼有一個(gè)缺點(diǎn),就是需要我們手動(dòng)調(diào)用unlock函數(shù)釋放鎖,這是一個(gè)安全隱患,并且,在某些情況下(異常),我們根本沒有機(jī)會(huì)自己手動(dòng)調(diào)用unlock函數(shù)。針對(duì)上面這種情況,c++引入了lock_guard類。
std::lock_guard
std::lock_guard使用RAII手法,在對(duì)象創(chuàng)建時(shí),自動(dòng)調(diào)用lock函數(shù),在對(duì)象銷毀時(shí),自動(dòng)調(diào)用unlock()函數(shù),從而保證互斥總能被正確解鎖。該類的實(shí)現(xiàn)很簡(jiǎn)單,直接貼源碼:
1 template <class _Mutex> 2 class _NODISCARD lock_guard { // class with destructor that unlocks a mutex 3 public: 4 using mutex_type = _Mutex; 5 6 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock 7 _MyMutex.lock(); 8 } 9 10 lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock 11 12 ~lock_guard() noexcept { 13 _MyMutex.unlock(); 14 } 15 16 lock_guard(const lock_guard&) = delete; 17 lock_guard& operator=(const lock_guard&) = delete; 18 19 private: 20 _Mutex& _MyMutex; 21 };
std::lock_guard僅提供了構(gòu)造函數(shù)和析構(gòu)函數(shù),并未提供其他成員函數(shù)。所以,我們只能用該函數(shù)來獲取鎖、釋放鎖。
案例:
1 int g_nResource = 0; 2 std::mutex _mutex; 3 void thread_entry() 4 { 5 lock_guard<mutex> lock(_mutex); 6 for (int i = 0; i < 10000000; ++i) 7 g_nResource++; 8 }
鎖的策略標(biāo)簽
std::lock_guard在構(gòu)造時(shí),可以傳入一個(gè)策略標(biāo)簽,用于標(biāo)識(shí)當(dāng)前鎖的狀態(tài),目前,有如下幾個(gè)標(biāo)簽,含義如下:
- std::defer_lock:表示不獲取互斥的所有權(quán)
- std::try_to_lock:嘗試獲得互斥的所有權(quán)而不阻塞
- std::adopt_lock:假設(shè)調(diào)用方線程已擁有互斥的所有權(quán)
這幾個(gè)標(biāo)簽可以為?std::lock_guard 、 std::unique_lock 和 std::shared_lock 指定鎖定策略。
用法如下:
1 std::lock(lhs._mutex, rhs._mutex); //對(duì)lhs、rhs上鎖 2 std::lock_guard<mutex> lock_a(lhs._mutex, std::adopt_lock); //不再上鎖 3 std::lock_guard<mutex> lock_b(rhs._mutex, std::adopt_lock); //不再上鎖
組織和編排代碼以保護(hù)共享數(shù)據(jù)
使用互斥并不是萬能的,一些情況還是可能會(huì)使得共享數(shù)據(jù)遭受破壞。例如:向調(diào)用者返回指針或引用,指向受保護(hù)的共享數(shù)據(jù),就會(huì)危及共享數(shù)據(jù)安全?;蛘撸陬悆?nèi)部調(diào)用其他外部接口,而該接口需要傳遞受保護(hù)對(duì)象的引用或者指針。例如:
1 class SomeData 2 { 3 public: 4 void DoSomething() { cout << "do something\n"; } 5 }; 6 7 class Operator 8 { 9 public: 10 void process(std::function<void(SomeData&)> func) 11 { 12 std::lock_guard<mutex> lock(_mutex); 13 func(data); //數(shù)據(jù)外溢 14 } 15 16 private: 17 SomeData data; 18 mutex _mutex; 19 }; 20 21 void GetDataPtr(SomeData** pPtr, SomeData& data) 22 { 23 *pPtr = &data; 24 } 25 26 int main() 27 { 28 Operator opt; 29 SomeData* pUnprotected = nullptr; 30 auto abk = [pUnprotected](SomeData& data) mutable 31 { 32 pUnprotected = &data; 33 }; 34 opt.process(abk); 35 pUnprotected->DoSomething(); //以無鎖形式訪問本應(yīng)該受到保護(hù)的數(shù)據(jù) 36 }
c++并未提供任何方法解決上面問題,歸根結(jié)底這是我們代碼設(shè)計(jì)的問題,需要牢記:不得向鎖所在的作用域之外傳遞指針和引用,指向受保護(hù)的共享數(shù)據(jù),無論是通過函數(shù)返回值將它們保存到對(duì)外可見的內(nèi)存,還是將它們作為參數(shù)傳遞給使用者提供的函數(shù)。
發(fā)現(xiàn)接口固有的條件競(jìng)爭(zhēng)
1 void func() 2 { 3 stack<int> s; 4 if (!s.empty()) 5 { 6 int nValue = s.top(); 7 s.pop(); 8 do_something(nValue); 9 } 10 }
在空棧上調(diào)用top()會(huì)導(dǎo)致未定義行為,上面的代碼已做好數(shù)據(jù)防備。對(duì)單線程而言,它既安全,又符合預(yù)期??墒?,只要涉及共享,這一連串調(diào)用便不再安全。因?yàn)椋趀mpty()和top()之間,可能有另一個(gè)線程調(diào)用pop(),彈出棧頂元素。毫無疑問,這正是典型的條件競(jìng)爭(zhēng)。它的根本原因在于函數(shù)接口,即使在內(nèi)部使用互斥保護(hù)棧容器中的元素,也無法防范。
消除返回值導(dǎo)致的條件競(jìng)爭(zhēng)的方法
方法一:傳入引用接收數(shù)據(jù)
template<typename T> class myStack { public: myStack(); ~myStack(); void pop(T& data); //傳入引用接收數(shù)據(jù) }; int main() { myStack<DataRes> s; DataRes result; s.pop(result); }
這在許多情況下行之有效,但還是有明顯短處。如果代碼要調(diào)用pop(),則須先依據(jù)棧容器中的元素類型構(gòu)造一個(gè)實(shí)例,將其充當(dāng)接收目標(biāo)傳入函數(shù)內(nèi)。對(duì)于某些類型,構(gòu)建實(shí)例的時(shí)間代價(jià)高昂或耗費(fèi)資源過多,所以不太實(shí)用。并且,該類型必須支持拷貝賦值運(yùn)算符。
方法二:提供不拋出異常的拷貝構(gòu)造函數(shù),或不拋出異常的移動(dòng)構(gòu)造函數(shù)
假設(shè)某個(gè)接口是按值返回,若它拋出異常,則牽涉異常安全的問題只會(huì)在這里出現(xiàn)。那么,只要確保構(gòu)造函數(shù)不會(huì)出現(xiàn)異常,該問題就可以解決。解決辦法是:讓該接口只允許哪些安全的類型返回。
方法三:返回指針,指向待返回元素
返回指針,指向彈出的元素,而不是返回它的值,其優(yōu)點(diǎn)是指針可以自由地復(fù)制,不會(huì)拋出異常??梢圆捎胹td::shared_ptr托管內(nèi)存資源。
方法四:結(jié)合方法一和方法二,或結(jié)合方法一和方法三
將上面幾種方法結(jié)合起來一起使用。
死鎖問題
線程在互斥上爭(zhēng)搶鎖,有兩個(gè)線程,都需要同時(shí)鎖住兩個(gè)互斥,可它們偏偏都只鎖住了一個(gè),都在等待另一把鎖,上述情況被稱為死鎖。
防范死鎖的建議是:始終按相同順序?qū)コ饧渔i。
1 class A 2 { 3 public: 4 A(int nValue) : m_nValue(nValue) {} 5 friend void Swap(A& lhs, A& rhs) 6 { 7 if (&lhs == &rhs) return; 8 lock_guard<mutex> lock_a(lhs._mutex); 9 lock_guard<mutex> lock_b(rhs._mutex); 10 std::swap(lhs.m_nValue, rhs.m_nValue); 11 } 12 private: 13 int m_nValue; 14 mutex _mutex; 15 }; 16 17 void func(A& lhs, A& rhs) 18 { 19 Swap(lhs, rhs); 20 } 21 22 int main() 23 { 24 A a1(10); 25 A a2(20); 26 thread th1(func, std::ref(a1), std::ref(a2)); //傳入?yún)?shù)順序不同 27 thread th2(func, std::ref(a2), std::ref(a1)); //傳入?yún)?shù)順序不同 28 th1.join(); 29 th2.join(); 30 }
上述代碼存在死鎖發(fā)生的可能。原因是在調(diào)用Swap時(shí),加鎖順序不一致,并且,上述例子出錯(cuò)更加的隱蔽,故障排除更困難。為此,c++提供了std::lock()函數(shù)。
std::lock()函數(shù)
該函數(shù)可以一次鎖住兩個(gè)或者兩個(gè)以上的互斥量。由于內(nèi)部算法的特性,它能避免因?yàn)槎鄠€(gè)線程加鎖順序不同導(dǎo)致死鎖的問題。用法如下:
1 class A 2 { 3 public: 4 A(int nValue) : m_nValue(nValue) {} 5 6 friend void Swap(A& lhs, A& rhs) 7 { 8 if (&lhs == &rhs) return; 9 std::lock(lhs._mutex, rhs._mutex); 10 std::lock_guard<mutex> lock_a(lhs._mutex, std::adopt_lock); //已經(jīng)上鎖,不再加鎖 11 std::lock_guard<mutex> lock_b(rhs._mutex, std::adopt_lock); //已經(jīng)上鎖,不再加鎖 12 std::swap(lhs.m_nValue, rhs.m_nValue); 13 } 14 15 private: 16 int m_nValue; 17 mutex _mutex; 18 };
std::scoped_lock類
c++17提供了scoped_lock類,該類的用法和std::lock_guard類相似,也是用于托管互斥量。二者區(qū)別在于scoped_lock類可以同時(shí)托管多個(gè)互斥。例如:
1 scoped_lock<mutex, mutex> lock(lhs._mutex, rhs._mutex);
由于c++17自帶類模板參數(shù)推導(dǎo),因此,上面代碼可以改寫為:
1 scoped_lock lock(lhs._mutex, rhs._mutex);
防范死鎖的補(bǔ)充準(zhǔn)則
雖然死鎖最常見的誘因之一是互斥操作,但即使沒有牽涉互斥,也會(huì)發(fā)生死鎖現(xiàn)象。例如:有兩個(gè)線程,各自關(guān)聯(lián)了std::thread實(shí)例,若它們同時(shí)在對(duì)方的std::thread實(shí)例上調(diào)用join(),就能制造出死鎖現(xiàn)象卻不涉及鎖操作。如果線程甲正等待線程乙完成某一動(dòng)作,同時(shí)線程乙卻在等待線程甲完成某一動(dòng)作,便會(huì)構(gòu)成簡(jiǎn)單的循環(huán)等待。防范死鎖的準(zhǔn)則最終可歸納成一個(gè)思想:只要另一線程有可能正在等待當(dāng)前線程,那么當(dāng)前線程千萬不能反過來等待它。
準(zhǔn)則1:避免嵌套鎖
假如已經(jīng)持有鎖,就不要試圖獲取第二個(gè)鎖,若每個(gè)線程最多只持有唯一一個(gè)鎖,那么對(duì)鎖的操作不會(huì)導(dǎo)致死鎖。萬一確有需要獲取多個(gè)鎖,我們應(yīng)采用std::lock()函數(shù),借單獨(dú)的調(diào)用動(dòng)作一次獲取全部鎖來避免死鎖。
準(zhǔn)則2:一旦持鎖,就須避免調(diào)用由用戶提供的程序接口
若程序接口由用戶自行實(shí)現(xiàn),則我們無從得知它到底會(huì)做什么,它可能會(huì)隨意操作,包括試圖獲取鎖。一旦我們已經(jīng)持鎖,若再調(diào)用由用戶提供的程序接口,而它恰好也要獲取鎖,此時(shí)就會(huì)導(dǎo)致死鎖。
準(zhǔn)則3:依次從固定順序獲取鎖
如果多個(gè)鎖是絕對(duì)必要的,卻無法通過std::lock()在一步操作中獲取全部的鎖,我們只能退而求其次,在每個(gè)線程內(nèi)部都依照固定順序獲取這些鎖,并確保所有線程都遵從。
準(zhǔn)則4:按層級(jí)加鎖
依照固定次序加鎖可能在實(shí)際中并不好執(zhí)行,那么,我們可以自己構(gòu)建一個(gè)層級(jí)鎖,根據(jù)鎖的層級(jí)結(jié)構(gòu)來進(jìn)行加鎖。但線程已經(jīng)獲取一個(gè)較低層的互斥鎖,那么,所有高于該層的互斥鎖全部不允許加鎖。
運(yùn)用std::unique_lock類靈活加鎖
std::unique_lock類同樣可以用來托管互斥量,但它比std::lock_guard類更加靈活,不一定始終占有與之關(guān)聯(lián)的互斥。
構(gòu)造函數(shù)
unique_lock(); unique_lock(_Mutex&); //構(gòu)造并調(diào)用lock上鎖 ~unique_lock(); //析構(gòu)并調(diào)用unlock解鎖 //構(gòu)造,_Mtx已經(jīng)被鎖,構(gòu)造函數(shù)不在調(diào)用lock unique_lock(_Mutex&, adopt_lock_t); //構(gòu)造,但不對(duì)_Mtx上鎖,需后續(xù)手動(dòng)調(diào)用 unique_lock(_Mutex&, defer_lock_t) //構(gòu)造,嘗試獲取鎖,不會(huì)造成阻塞 unique_lock(_Mutex&, try_to_lock_t) //構(gòu)造 + try_lock_shared_for unique_lock(_Mutex&, const chrono::duration<_Rep, _Period>&); //構(gòu)造 + try_lock_shared_until unique_lock(_Mutex&, const chrono::time_point<_Clock, _Duration>&); unique_lock(unique_lock&& _Other); //移動(dòng)構(gòu)造 //若占有則解鎖互斥,并取得另一者的所有權(quán) unique_lock& operator=(unique_lock&& _Other); //無拷貝構(gòu)造 unique_lock(const unique_lock&) = delete; unique_lock& operator=(const unique_lock&) = delete;
構(gòu)造函數(shù)提供了靈活的加鎖策略。
成員函數(shù)
//鎖定關(guān)聯(lián)互斥 void lock(); //解鎖關(guān)聯(lián)互斥 void unlock(); //嘗試鎖定關(guān)聯(lián)互斥,若互斥不可用則返回 bool try_lock(); //試圖鎖定關(guān)聯(lián)的可定時(shí)鎖定 (TimedLockable) 互斥,若互斥在給定時(shí)長中不可用則返回 bool try_lock_for(const chrono::duration<_Rep, _Period>&); //嘗試鎖定關(guān)聯(lián)可定時(shí)鎖定 (TimedLockable) 互斥,若抵達(dá)指定時(shí)間點(diǎn)互斥仍不可用則返回 bool try_lock_until(const chrono::time_point<_Clock, _Duration>&); //與另一 std::unique_lock 交換狀態(tài) void swap(unique_lock& _Other); //將關(guān)聯(lián)互斥解關(guān)聯(lián)而不解鎖它 _Mutex* release(); //測(cè)試是否占有其關(guān)聯(lián)互斥 bool owns_lock(); //同owns_lock operator bool(); //返回指向關(guān)聯(lián)互斥的指針 _Mutex* mutex();
提供了lock()、unlock()等接口,可以隨時(shí)解鎖或者上鎖。
在不同的作用域之間轉(zhuǎn)移互斥歸屬權(quán)
因?yàn)閟td::unique_lock實(shí)例不占有與之關(guān)聯(lián)的互斥,所以隨著其實(shí)例的轉(zhuǎn)移,互斥的歸屬權(quán)可以在多個(gè)std::unique_lock實(shí)例之間轉(zhuǎn)移。通過移動(dòng)語義完成,注意區(qū)分左值和右值。
轉(zhuǎn)移有一種用途:準(zhǔn)許函數(shù)鎖定互斥,然后把互斥的歸屬權(quán)轉(zhuǎn)移給函數(shù)調(diào)用者,好讓他在同一個(gè)鎖的保護(hù)下執(zhí)行其他操作。代碼如下:
1 std::mutex _Mtx; 2 3 void PrepareData() {} 4 5 void DoSomething() {} 6 7 std::unique_lock<std::mutex> get_lock() 8 { 9 std::unique_lock<std::mutex> lock(_Mtx); 10 PrepareData(); 11 return lock; 12 } 13 14 void ProcessData() 15 { 16 std::unique_lock<std::mutex> lock(get_lock()); 17 DoSomething(); 18 }
按適合的粒度加鎖
“鎖粒度”該術(shù)語描述一個(gè)鎖所保護(hù)的數(shù)據(jù)量。粒度精細(xì)的鎖保護(hù)少量數(shù)據(jù),而粒度粗大的鎖保護(hù)大量數(shù)據(jù)。鎖操作有兩個(gè)要點(diǎn):一是選擇足夠粗大的鎖粒度,確保目標(biāo)數(shù)據(jù)都受到保護(hù);二是限制范圍,務(wù)求只在必要的操作過程中持鎖。只要條件允許,我們僅僅在訪問共享數(shù)據(jù)期間才鎖住互斥,讓數(shù)據(jù)處理盡可能不用鎖保護(hù)。持鎖期間應(yīng)避免任何耗時(shí)的操作,如讀寫文件。這種情況可用std::unique_lock處理:假如代碼不再需要訪問共享數(shù)據(jù),那我們就調(diào)用unlock()解鎖;若以后需重新訪問,則調(diào)用lock()加鎖。
1 std::mutex _Mtx; 2 bool GetAndProcessData() 3 { 4 std::unique_lock<std::mutex> lock(_Mtx); 5 DataResource data = GetData(); 6 lock.unlock(); 7 bool bResult = WirteToFile(data); //非常耗時(shí) 8 lock.lock(); 9 SaveResult(bResult); 10 return bResult; 11 }
一般地,若要執(zhí)行某項(xiàng)操作,那我們應(yīng)該只在所需的最短時(shí)間內(nèi)持鎖。換言之,除非絕對(duì)必要,否則不得在持鎖期間進(jìn)行耗時(shí)的操作,如等待I/O完成或獲取另一個(gè)鎖(即便我們知道不會(huì)死鎖)。例如,在比較運(yùn)算的過程中,每次只鎖住一個(gè)互斥:
1 class Y 2 { 3 private: 4 int some_detail; 5 mutable std::mutex m; 6 int get_detail() const 7 { 8 std::lock_guard<std::mutex> lock_a(m); 9 return some_detail; 10 } 11 public: 12 Y(int sd):some_detail(sd){} 13 friend bool operator==(Y const& lhs, Y const& rhs) 14 { 15 if(&lhs==&rhs) 16 return true; 17 int const lhs_value=lhs.get_detail(); 18 int const rhs_value=rhs.get_detail(); 19 return lhs_value==rhs_value; ?--- ④ 20 } 21 };
為了縮短持鎖定的時(shí)間,我們一次只持有一個(gè)鎖。
保護(hù)共享數(shù)據(jù)的其他工具
互斥是保護(hù)共享數(shù)據(jù)的最普遍的方式之一,但它并非唯一方式。
在初始化過程中保護(hù)共享數(shù)據(jù)
假設(shè)我們需要某個(gè)共享數(shù)據(jù),而它創(chuàng)建起來開銷不菲。因?yàn)閯?chuàng)建它可能需要建立數(shù)據(jù)庫連接或分配大量?jī)?nèi)存,所以等到必要時(shí)才真正著手創(chuàng)建。這種方式稱為延遲初始化(lazy initialization)。最常見的就是實(shí)現(xiàn)懶漢式單例模式,現(xiàn)在,時(shí)代變了,實(shí)現(xiàn)線程安全的單例模式,不需要使用雙重鎖了!
std::call_once()函數(shù)與std::once_flag
std::call_once()函數(shù)可以確??烧{(diào)用對(duì)象僅執(zhí)行一次,即使是在并發(fā)訪問下。該函數(shù)定義如下:
1 template <class _Fn, class... _Args> 2 void(call_once)(once_flag& _Once, _Fn&& _Fx, _Args&&... _Ax);
- _Once:std::once_flag對(duì)象,它確保僅有一個(gè)線程能執(zhí)行函數(shù)。
- _Fx:待調(diào)用的可調(diào)用對(duì)象。
- _Ax:傳遞給可調(diào)用對(duì)象的參數(shù)包。
用std::call_once()函數(shù)實(shí)現(xiàn)單例:
1 class Singleton 2 { 3 public: 4 static Singleton* Ins() 5 { 6 std::call_once(_flag, []() { 7 _ins = new Singleton; 8 }); 9 return _ins; 10 } 11 12 Singleton(const Singleton&) = delete; 13 Singleton& operator=(const Singleton&) = delete; 14 15 protected: 16 Singleton() { std::cout << "constructor" << std::endl; } 17 ~Singleton() { std::cout << "destructor" << std::endl; } //必須聲明為私有,否則返回指針將可析構(gòu) 18 19 private: 20 struct Deleter 21 { 22 ~Deleter() { 23 delete _ins; 24 _ins = nullptr; 25 } 26 }; 27 static Deleter _deleter; 28 static Singleton* _ins; 29 static std::once_flag _flag; 30 }; 31 32 Singleton::Deleter Singleton::_deleter; 33 Singleton* Singleton::_ins = nullptr; 34 std::once_flag Singleton::_flag;
Deleter確保Singleton對(duì)象銷毀時(shí),能夠釋放_ins對(duì)象。
Magic Static特性
C++11標(biāo)準(zhǔn)中定義了一個(gè)Magic Static特性:如果變量當(dāng)前處于初始化狀態(tài),當(dāng)發(fā)生并發(fā)訪問時(shí),并發(fā)線程將會(huì)阻塞,等待初始化結(jié)束。
用Magic Static特性實(shí)現(xiàn)單例:
1 class Singleton 2 { 3 public: 4 static Singleton& Ins() 5 { 6 static Singleton _ins; 7 return _ins; 8 } 9 10 Singleton(const Singleton&) = delete; 11 Singleton& operator=(const Singleton&) = delete; 12 13 protected: 14 Singleton() { std::cout << "constructor" << std::endl; } 15 ~Singleton() { std::cout << "destructor" << std::endl; } 16 };
保護(hù)甚少更新的數(shù)據(jù)結(jié)構(gòu)
考慮一個(gè)存儲(chǔ)著DNS條目的緩存表,它將域名解釋成對(duì)應(yīng)的IP地址。給定的DNS條目通常在很長時(shí)間內(nèi)都不會(huì)變化——在許多情況下,DNS條目保持多年不變。盡管,隨著用戶訪問不同網(wǎng)站,緩存表會(huì)不時(shí)加入新條目,但在很大程度上,數(shù)據(jù)在整個(gè)生命期內(nèi)將保持不變。為了判斷數(shù)據(jù)是否有效,必須定期查驗(yàn)緩存表;只要細(xì)節(jié)有所改動(dòng),就需要進(jìn)行更新。
更新雖然鮮有,但它們還是會(huì)發(fā)生。另外,如果緩存表被多線程訪問,更新過程就需得到妥善保護(hù),以確保各個(gè)線程在讀取緩存表時(shí),全都見不到失效數(shù)據(jù)。
如果使用傳統(tǒng)的互斥,效率可能不高:當(dāng)更新緩存表時(shí),阻止其他線程訪問數(shù)據(jù)是理所應(yīng)到。但很多時(shí)候,數(shù)據(jù)未發(fā)生改變,但每個(gè)線程讀取數(shù)據(jù)都會(huì)導(dǎo)致上鎖,即讀多寫少,std::mutex效率就比較低了。
C++17標(biāo)準(zhǔn)庫提供了兩種新的互斥:std::shared_mutex和std::shared_timed_mutex。
std::shared_mutex
- 平臺(tái):c++17
- 頭文件:?<shared_mutex>?
std::shared_mutex類可用于保護(hù)共享數(shù)據(jù)不被多個(gè)線程同時(shí)訪問。與獨(dú)占式互斥不同,該類擁有兩種訪問級(jí)別:
- 共享 - 多個(gè)線程能共享同一互斥的所有權(quán)。
- 獨(dú)占性 - 僅一個(gè)線程能占有互斥。
std::shared_mutex有如下特點(diǎn):
- 若一個(gè)線程已獲得獨(dú)占鎖(通過lock、try_lock)則無其他線程能獲取該鎖(包括共享的)。
- 僅當(dāng)任何線程均未獲取獨(dú)占性鎖時(shí),共享鎖才能被多個(gè)線程獲?。ㄍㄟ^lock_shared 、try_lock_shared)。
- 在一個(gè)線程內(nèi),同一時(shí)刻只能獲取一個(gè)鎖(共享或獨(dú)占性)。
構(gòu)造函數(shù)
shared_mutex(); //構(gòu)造互斥 ~shared_mutex(); //析構(gòu)互斥 //無拷貝 shared_mutex(const shared_mutex&) = delete; shared_mutex& operator=(const shared_mutex&) = delete;
獨(dú)占鎖
void lock(); //鎖定互斥,若互斥不可用則阻塞 void unlock(); //解鎖互斥 void try_lock(); //嘗試鎖定互斥,若互斥不可用則返回
共享鎖
void lock_shared(); //為共享所有權(quán)鎖定互斥,若互斥不可用則阻塞 bool try_lock_shared(); //嘗試為共享所有權(quán)鎖定互斥,若互斥不可用則返回 void unlock_shared(); //解鎖共享所有權(quán)互斥
案例
1 std::shared_mutex _Mtx; 2 void func() 3 { 4 _Mtx.lock_shared(); 5 cout << " thread Id = " << this_thread::get_id() << " do something!\n"; 6 _Mtx.unlock_shared(); 7 } 8 9 int main() 10 { 11 _Mtx.lock_shared(); //使用共享鎖鎖住 12 thread th1(func); 13 thread th2(func); 14 th1.join(); 15 th2.join(); 16 _Mtx.unlock_shared(); 17 }
main函數(shù)中使用共享鎖鎖住,實(shí)際并不影響其他線程獲取共享鎖,如果將main函數(shù)中的共享鎖換成獨(dú)占鎖,程序?qū)l(fā)生死鎖。同理,如果將func函數(shù)中的共享鎖換成獨(dú)占鎖,同樣會(huì)造成死鎖,獲取獨(dú)占鎖時(shí),如果當(dāng)前有其他線程正持有共享鎖,那么該線程將阻塞,直到其他線程釋放共享鎖。
std::shared_timed_mutex
- 平臺(tái):c++14
- 頭文件:?<shared_mutex>?
與std::shared_mutex類相似,只是提供了額外的成員函數(shù)。
構(gòu)造函數(shù)
shared_timed_mutex(); ~shared_timed_mutex(); shared_timed_mutex(const shared_timed_mutex&) = delete; shared_timed_mutex& operator=(const shared_timed_mutex&) = delete;
獨(dú)占鎖
void lock(); //鎖定互斥,若互斥不可用則阻塞 void unlock(); //解鎖互斥 bool try_lock(); //嘗試鎖定互斥,若互斥不可用則返回 //嘗試鎖定互斥,若互斥在指定的時(shí)限時(shí)期中不可用則返回 bool try_lock_for(const chrono::duration<_Rep, _Period>&); //嘗試鎖定互斥,若直至抵達(dá)指定時(shí)間點(diǎn)互斥不可用則返回 bool try_lock_until(const chrono::time_point<_Clock, _Duration>&)
共享鎖
void lock_shared(); //為共享所有權(quán)鎖定互斥,若互斥不可用則阻塞 bool try_lock_shared(); //嘗試為共享所有權(quán)鎖定互斥,若互斥不可用則返回 void unlock_shared(); //解鎖互斥(共享所有權(quán)) //嘗試為共享所有權(quán)鎖定互斥,若互斥在指定的時(shí)限時(shí)期中不可用則返回 bool try_lock_shared_for(const chrono::duration<_Rep, _Period>&); //嘗試為共享所有權(quán)鎖定互斥,若直至抵達(dá)指定時(shí)間點(diǎn)互斥不可用則返回 bool try_lock_shared_until(const chrono::time_point<_Clock, _Duration>&);
std::shared_lock
std::shared_lock和std::unique_lock類相似,unique_lock用于操作獨(dú)占鎖,其構(gòu)造函數(shù)將調(diào)用lock()函數(shù),析構(gòu)函數(shù)將調(diào)用unlock()函數(shù)。shared_lock用于操作共享鎖,其構(gòu)造函數(shù)將調(diào)用lock_shared()函數(shù),析構(gòu)函數(shù)將調(diào)用unlock_shared()函數(shù)。
構(gòu)造函數(shù)
shared_lock(); shared_lock(mutex_type&); //構(gòu)造并調(diào)用lock_shared上鎖 ~shared_lock(); //析構(gòu)并調(diào)用unlock_shared解鎖 //構(gòu)造,但不對(duì)_Mtx上鎖,需后續(xù)手動(dòng)調(diào)用 shared_lock(mutex_type&, defer_lock_t) //構(gòu)造,嘗試獲取鎖,不會(huì)造成阻塞 shared_lock(mutex_type&, try_to_lock_t) //構(gòu)造,_Mtx已經(jīng)被鎖,構(gòu)造函數(shù)不在調(diào)用lock shared_lock(mutex_type&, adopt_lock_t) //構(gòu)造 + try_lock_shared_for shared_lock(mutex_type&, const chrono::duration<_Rep, _Period>&) //構(gòu)造 + try_lock_shared_until shared_lock(mutex_type&, const chrono::time_point<_Clock, _Duration>&) shared_lock(shared_lock&&); //移動(dòng)構(gòu)造 shared_lock& operator=(shared_lock&&); //移動(dòng)賦值,會(huì)先解鎖
成員函數(shù)
//鎖定關(guān)聯(lián)的互斥 void lock(); //嘗試鎖定關(guān)聯(lián)的互斥 bool try_lock(); //解鎖關(guān)聯(lián)的互斥 void unlock(); //嘗試鎖定關(guān)聯(lián)的互斥,以指定時(shí)長 try_lock_for(const chrono::duration<_Rep, _Period>&); //嘗試鎖定關(guān)聯(lián)的互斥,直至指定的時(shí)間點(diǎn) bool try_lock_until(const chrono::time_point<_Clock, _Duration>&); //解除關(guān)聯(lián) mutex 而不解鎖 mutex_type* release(); //測(cè)試鎖是否占有其關(guān)聯(lián)的互斥 bool owns_lock(); //同owns_lock operator bool(); //返回指向關(guān)聯(lián)的互斥的指針 mutex_type* mutex(); //與另一 shared_lock 交換數(shù)據(jù)成員 void swap(shared_lock& _Right)
案例
1 class A 2 { 3 public: 4 A& operator=(const A& other) 5 { 6 //上獨(dú)占鎖(寫操作) 7 unique_lock<shared_mutex> lhs(_Mtx, defer_lock); 8 9 //上共享鎖(讀操作) 10 shared_lock<shared_mutex> rhs(other._Mtx, defer_lock); 11 12 //上鎖 13 lock(lhs, rhs); 14 15 to_do_assignment(); //賦值操作 16 return *this; 17 } 18 private: 19 mutable std::shared_mutex _Mtx; 20 };
遞歸加鎖
假如線程已經(jīng)持有某個(gè)std::mutex實(shí)例,試圖再次對(duì)其重新加鎖就會(huì)出錯(cuò),將導(dǎo)致未定義行為。但在某些場(chǎng)景中,確有需要讓線程在同一互斥上多次重復(fù)加鎖,而無須解鎖。C++標(biāo)準(zhǔn)庫為此提供了std::recursive_mutex,其工作方式與std::mutex相似,不同之處是,其允許同一線程對(duì)某互斥的同一實(shí)例多次加鎖。我們必須先釋放全部的鎖,才可以讓另一個(gè)線程鎖住該互斥。例如,若我們對(duì)它調(diào)用了3次lock(),就必須調(diào)用3次unlock()。只要正確地使用std::lock_guard<std::recursive_mutex>和std::unique_lock<std::recursive_mutex>,它們便會(huì)處理好遞歸鎖的余下細(xì)節(jié)。
工作中盡量避免使用遞歸鎖,這可能是一種拙劣的設(shè)計(jì),換一種方式,可能用普通鎖就解決問題了。比如,提取一個(gè)新的函數(shù),在外部先加鎖,然后遞歸調(diào)用該函數(shù)。文章來源:http://www.zghlxwxcb.cn/news/detail-709759.html
Copyright
本文參考至《c++并發(fā)編程實(shí)戰(zhàn)》 第二版,作者:安東尼·威廉姆斯。本人閱讀后添加了自己的理解并整理,方便后續(xù)查找,可能存在錯(cuò)誤,歡迎大家指正,感謝!文章來源地址http://www.zghlxwxcb.cn/news/detail-709759.html
到了這里,關(guān)于c++并發(fā)編程實(shí)戰(zhàn)-第3章 在線程間共享數(shù)據(jù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!