国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

c++并發(fā)編程實(shí)戰(zhàn)-第3章 在線程間共享數(shù)據(jù)

這篇具有很好參考價(jià)值的文章主要介紹了c++并發(fā)編程實(shí)戰(zhàn)-第3章 在線程間共享數(shù)據(jù)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

線程間共享數(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ù)。

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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • C++并發(fā)線程 - 如何線程間共享數(shù)據(jù)【詳解:如何使用鎖操作】

    C++并發(fā)線程 - 如何線程間共享數(shù)據(jù)【詳解:如何使用鎖操作】

    點(diǎn)擊進(jìn)入系列文章目錄 C++技能系列 Linux通信架構(gòu)系列 C++高性能優(yōu)化編程系列 深入理解軟件架構(gòu)設(shè)計(jì)系列 高級(jí)C++并發(fā)線程編程 期待你的關(guān)注哦?。?! 快樂在于態(tài)度,成功在于細(xì)節(jié),命運(yùn)在于習(xí)慣。 Happiness lies in the attitude, success lies in details, fate is a habit. 具體哪個(gè)線程按何種

    2024年02月08日
    瀏覽(26)
  • C++線程入門:輕松并發(fā)編程

    ????????在現(xiàn)代計(jì)算機(jī)應(yīng)用程序中,我們經(jīng)常需要處理并發(fā)任務(wù),這就需要使用多線程來實(shí)現(xiàn)。C++是一種功能強(qiáng)大的編程語言,提供了豐富的線程支持,使得并發(fā)編程變得相對(duì)容易。 ????????C++ 線程是一種多線程編程模型,可以在同一個(gè)程序中同時(shí)執(zhí)行多個(gè)獨(dú)立的任務(wù)

    2024年02月04日
    瀏覽(21)
  • c++并發(fā)編程實(shí)戰(zhàn)-第4章 并發(fā)操作的同步

    c++并發(fā)編程實(shí)戰(zhàn)-第4章 并發(fā)操作的同步

    想象一種情況:假設(shè)晚上坐車外出,如何才能確保不坐過站又能使自己最輕松? 這種方式存在雙重浪費(fèi): 線程 th1(wait_for_flag)須不斷查驗(yàn)標(biāo)志,浪費(fèi)原本有用的處理時(shí)間,這部分計(jì)算資源原本可以留給其他線程使用。 線程 th1(wait_for_flag)每次循環(huán)都需要給互斥上鎖,導(dǎo)致

    2024年02月08日
    瀏覽(19)
  • 《C++并發(fā)編程實(shí)戰(zhàn)》讀書筆記(3):并發(fā)操作的同步

    當(dāng)線程需要等待特定事件發(fā)生、或是某個(gè)條件成立時(shí),可以使用條件變量 std::condition_variable ,它在標(biāo)準(zhǔn)庫頭文件 condition_variable 內(nèi)聲明。 wait() 會(huì)先在內(nèi)部調(diào)用lambda函數(shù)判斷條件是否成立,若條件成立則 wait() 返回,否則解鎖互斥并讓當(dāng)前線程進(jìn)入等待狀態(tài)。當(dāng)其它線程調(diào)用

    2024年02月10日
    瀏覽(22)
  • 《C++并發(fā)編程實(shí)戰(zhàn)》讀書筆記(4):原子變量

    標(biāo)準(zhǔn)原子類型的定義位于頭文件 atomic 內(nèi)。原子操作的關(guān)鍵用途是取代需要互斥的同步方式,但假設(shè)原子操作本身也在內(nèi)部使用了互斥,就很可能無法達(dá)到期望的性能提升。有三種方法來判斷一個(gè)原子類型是否屬于無鎖數(shù)據(jù)結(jié)構(gòu): 所有標(biāo)準(zhǔn)原子類型( std::atomic_flag 除外,因?yàn)?/p>

    2024年02月10日
    瀏覽(28)
  • C++11并發(fā)與多線程筆記(7) 單例設(shè)計(jì)模式共享數(shù)據(jù)分析、解決,call_once

    程序靈活,維護(hù)起來可能方便,用設(shè)計(jì)模式理念寫出來的代碼很晦澀,但是別人接管、閱讀代碼都會(huì)很痛苦 老外應(yīng)付特別大的項(xiàng)目時(shí),把項(xiàng)目的開發(fā)經(jīng)驗(yàn)、模塊劃分經(jīng)驗(yàn),總結(jié)整理成設(shè)計(jì)模式 中國零幾年設(shè)計(jì)模式剛開始火時(shí),總喜歡拿一個(gè)設(shè)計(jì)模式往上套,導(dǎo)致一個(gè)小小的

    2024年02月12日
    瀏覽(23)
  • 【并發(fā)編程】線程池多線程異步去分頁調(diào)用其他服務(wù)接口獲取海量數(shù)據(jù)

    前段時(shí)間在做一個(gè)數(shù)據(jù)同步工具,其中一個(gè)服務(wù)的任務(wù)是調(diào)用A服務(wù)的接口,將數(shù)據(jù)庫中指定數(shù)據(jù)請(qǐng)求過來,交給kafka去判斷哪些數(shù)據(jù)是需要新增,哪些數(shù)據(jù)是需要修改的。 剛開始的設(shè)計(jì)思路是,,我創(chuàng)建多個(gè)服務(wù)同時(shí)去請(qǐng)求A服務(wù)的接口,每個(gè)服務(wù)都請(qǐng)求到全量數(shù)據(jù),由于這些

    2024年02月13日
    瀏覽(32)
  • 【并發(fā)編程】自研數(shù)據(jù)同步工具的優(yōu)化:創(chuàng)建線程池多線程異步去分頁調(diào)用其他服務(wù)接口獲取海量數(shù)據(jù)

    前段時(shí)間在做一個(gè)數(shù)據(jù)同步工具,其中一個(gè)服務(wù)的任務(wù)是調(diào)用A服務(wù)的接口,將數(shù)據(jù)庫中指定數(shù)據(jù)請(qǐng)求過來,交給kafka去判斷哪些數(shù)據(jù)是需要新增,哪些數(shù)據(jù)是需要修改的。 剛開始的設(shè)計(jì)思路是,,我創(chuàng)建多個(gè)服務(wù)同時(shí)去請(qǐng)求A服務(wù)的接口,每個(gè)服務(wù)都請(qǐng)求到全量數(shù)據(jù),由于這些

    2024年02月12日
    瀏覽(23)
  • Java并發(fā)編程——偽共享和緩存行問題

    Java并發(fā)編程——偽共享和緩存行問題

    在Java并發(fā)編程中,偽共享(False Sharing)和緩存行(Cache Line)是與多線程訪問共享數(shù)據(jù)相關(guān)的兩個(gè)重要概念。 偽共享指的是多個(gè)線程同時(shí)訪問同一個(gè)緩存行中的不同變量或數(shù)據(jù),其中至少一個(gè)線程對(duì)其中一個(gè)變量進(jìn)行寫操作。由于處理器緩存行的一致性協(xié)議要求緩存行中的數(shù)

    2024年02月02日
    瀏覽(14)
  • 并發(fā)編程(高并發(fā)、多線程)

    并發(fā)編程(高并發(fā)、多線程)

    1.1.1 并發(fā)編程三要素 首先我們要了解并發(fā)編程的三要素 原子性 可見性 有序性 1.原子性 原子性是指一個(gè)操作是不可分割的單元,要么全部執(zhí)行成功,要么全部失敗。 在并發(fā)環(huán)境中,多個(gè)線程可能同時(shí)訪問和修改共享的數(shù)據(jù),為了確保數(shù)據(jù)的一致性,需要確保一組相關(guān)的操作

    2024年02月04日
    瀏覽(24)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包