C++多線程編程時(shí)的數(shù)據(jù)保護(hù)
在編寫(xiě)多線程程序時(shí),多個(gè)線程同時(shí)訪問(wèn)某個(gè)共享資源,會(huì)導(dǎo)致同步的問(wèn)題,這篇文章中我們將介紹 C++11 多線程編程中的數(shù)據(jù)保護(hù)。
數(shù)據(jù)丟失
讓我們從一個(gè)簡(jiǎn)單的例子開(kāi)始,請(qǐng)看如下代碼:
#include <iostream> #include <string> #include <thread> #include <vector> using std::thread; using std::vector; using std::cout; using std::endl; class Incrementer { private: int counter; public: Incrementer() : counter{0} { }; void operator()() { for(int i = 0; i < 100000; i++) { this->counter++; } } int getCounter() const { return this->counter; } }; int main() { // Create the threads which will each do some counting vector<thread> threads; Incrementer counter; threads.push_back(thread(std::ref(counter))); threads.push_back(thread(std::ref(counter))); threads.push_back(thread(std::ref(counter))); for(auto &t : threads) { t.join(); } cout << counter.getCounter() << endl; return 0; }
這個(gè)程序的目的就是數(shù)數(shù),數(shù)到30萬(wàn),某些傻叉程序員想要優(yōu)化數(shù)數(shù)的過(guò)程,因此創(chuàng)建了三個(gè)線程,使用一個(gè)共享變量 counter,每個(gè)線程負(fù)責(zé)給這個(gè)變量增加10萬(wàn)計(jì)數(shù)。
這段代碼創(chuàng)建了一個(gè)名為 Incrementer 的類(lèi),該類(lèi)包含一個(gè)私有變量 counter,其構(gòu)造器非常簡(jiǎn)單,只是將 counter 設(shè)置為 0.
緊接著是一個(gè)操作符重載,這意味著這個(gè)類(lèi)的每個(gè)實(shí)例都是被當(dāng)作一個(gè)簡(jiǎn)單函數(shù)來(lái)調(diào)用的。一般我們調(diào)用類(lèi)的某個(gè)方法時(shí)會(huì)這樣 object.fooMethod(),但現(xiàn)在你實(shí)際上是直接調(diào)用了對(duì)象,如object(). 因?yàn)槲覀兪窃诓僮鞣剌d函數(shù)中將整個(gè)對(duì)象傳遞給了線程類(lèi)。最后是一個(gè) getCounter 方法,返回 counter 變量的值。
再下來(lái)是程序的入口函數(shù) main(),我們創(chuàng)建了三個(gè)線程,不過(guò)只創(chuàng)建了一個(gè) Incrementer 類(lèi)的實(shí)例,然后將這個(gè)實(shí)例傳遞給三個(gè)線程,注意這里使用了 std::ref ,這相當(dāng)于是傳遞了實(shí)例的引用對(duì)象,而不是對(duì)象的拷貝。
現(xiàn)在讓我們來(lái)看看程序執(zhí)行的結(jié)果,如果這位傻叉程序員還夠聰明的話(huà),他會(huì)使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 來(lái)進(jìn)行編譯,編譯方法:
g++ -std=c++11 -lpthread -o threading_example main.cpp
運(yùn)行結(jié)果:
[lucas@lucas-desktop src]$ ./threading_example 218141 [lucas@lucas-desktop src]$ ./threading_example 208079 [lucas@lucas-desktop src]$ ./threading_example 100000 [lucas@lucas-desktop src]$ ./threading_example 202426 [lucas@lucas-desktop src]$ ./threading_example 172209
但等等,不對(duì)啊,程序并沒(méi)有數(shù)數(shù)到30萬(wàn),有一次居然只數(shù)到10萬(wàn),為什么會(huì)這樣呢?好吧,加1操作對(duì)應(yīng)實(shí)際的處理器指令其實(shí)包括:
movl counter(%rip), %eax addl $1, %eax movl %eax, counter(%rip)
首個(gè)指令將裝載 counter 的值到 %eax 寄存器,緊接著寄存器的值增1,然后將寄存器的值移給內(nèi)存中 counter 所在的地址。
我聽(tīng)到你在嘀咕:這不錯(cuò),可為什么會(huì)導(dǎo)致數(shù)數(shù)錯(cuò)誤的問(wèn)題呢?嗯,還記得我們以前說(shuō)過(guò)線程會(huì)共享處理器,因?yàn)橹挥袉魏?。因此在某些點(diǎn)上,一個(gè)線程會(huì)依照指令執(zhí)行完成,但在很多情況下,操作系統(tǒng)會(huì)對(duì)線程說(shuō):時(shí)間結(jié)束了,到后面排隊(duì)再來(lái),然后另外一個(gè)線程開(kāi)始執(zhí)行,當(dāng)下一個(gè)線程開(kāi)始執(zhí)行時(shí),它會(huì)從被暫停的那個(gè)位置開(kāi)始執(zhí)行。所以你猜會(huì)發(fā)生什么事,當(dāng)前線程正準(zhǔn)備執(zhí)行寄存器加1操作時(shí),系統(tǒng)把處理器交給另外一個(gè)線程?
我真的不知道會(huì)發(fā)生什么事,可能我們?cè)跍?zhǔn)備加1時(shí),另外一個(gè)線程進(jìn)來(lái)了,重新將 counter 值加載到寄存器等多種情況的產(chǎn)生。誰(shuí)也不知道到底發(fā)生了什么。
正確的做法
解決方案就是要求同一個(gè)時(shí)間內(nèi)只允許一個(gè)線程訪問(wèn)共享變量。這個(gè)可通過(guò) std::mutex 類(lèi)來(lái)解決。當(dāng)線程進(jìn)入時(shí),加鎖、執(zhí)行操作,然后釋放鎖。其他線程想要訪問(wèn)這個(gè)共享資源必須等待鎖釋放。
互斥(mutex) 是操作系統(tǒng)確保鎖和解鎖操作是不可分割的。這意味著線程在對(duì)互斥量進(jìn)行鎖和解鎖的操作是不會(huì)被中斷的。當(dāng)線程對(duì)互斥量進(jìn)行鎖或者解鎖時(shí),該操作會(huì)在操作系統(tǒng)切換線程前完成。
而最好的事情是,當(dāng)你試圖對(duì)互斥量進(jìn)行加鎖操作時(shí),其他的線程已經(jīng)鎖住了該互斥量,那你就必須等待直到其釋放。操作系統(tǒng)會(huì)跟蹤哪個(gè)線程正在等待哪個(gè)互斥量,被堵塞的線程會(huì)進(jìn)入 "blocked onm" 狀態(tài),意味著操作系統(tǒng)不會(huì)給這個(gè)堵塞的線程任何處理器時(shí)間,直到互斥量解鎖,因此也不會(huì)浪費(fèi) CPU 的循環(huán)。如果有多個(gè)線程處于等待狀態(tài),哪個(gè)線程最先獲得資源取決于操作系統(tǒng)本身,一般像 Windows 和 Linux 系統(tǒng)使用的是 FIFO 策略,在實(shí)時(shí)操作系統(tǒng)中則是基于優(yōu)先級(jí)的。
現(xiàn)在讓我們對(duì)上面的代碼進(jìn)行改進(jìn):
#include <iostream> #include <string> #include <thread> #include <vector> #include <mutex> using std::thread; using std::vector; using std::cout; using std::endl; using std::mutex; class Incrementer { private: int counter; mutex m; public: Incrementer() : counter{0} { }; void operator()() { for(int i = 0; i < 100000; i++) { this->m.lock(); this->counter++; this->m.unlock(); } } int getCounter() const { return this->counter; } }; int main() { // Create the threads which will each do some counting vector<thread> threads; Incrementer counter; threads.push_back(thread(std::ref(counter))); threads.push_back(thread(std::ref(counter))); threads.push_back(thread(std::ref(counter))); for(auto &t : threads) { t.join(); } cout << counter.getCounter() << endl; return 0; }
注意代碼上的變化:我們引入了 mutex 頭文件,增加了一個(gè) m 的成員,類(lèi)型是 mutex,在operator()() 中我們鎖住互斥量 m 然后對(duì) counter 進(jìn)行加1操作,然后釋放互斥量。
再次執(zhí)行上述程序,結(jié)果如下:
[lucas@lucas-desktop src]$ ./threading_example 300000 [lucas@lucas-desktop src]$ ./threading_example 300000
這下數(shù)對(duì)了。不過(guò)在計(jì)算機(jī)科學(xué)中,沒(méi)有免費(fèi)的午餐,使用互斥量會(huì)降低程序的性能,但這總比一個(gè)錯(cuò)誤的程序要強(qiáng)吧。
防范異常
當(dāng)對(duì)變量進(jìn)行加1操作時(shí),是可能會(huì)發(fā)生異常的,當(dāng)然在我們這個(gè)例子中發(fā)生異常的機(jī)會(huì)微乎其微,但是在一些復(fù)雜系統(tǒng)中是極有可能的。上面的代碼并不是異常安全的,當(dāng)異常發(fā)生時(shí),程序已經(jīng)結(jié)束了,可是互斥量還是處于鎖的狀態(tài)。
為了確?;コ饬吭诋惓0l(fā)生的情況下也能被解鎖,我們需要使用如下代碼:
for(int i = 0; i < 100000; i++) { this->m.lock(); try { this->counter++; this->m.unlock(); } catch(...) { this->m.unlock(); throw; } }
但是,這代碼太多了,而只是為了對(duì)互斥量進(jìn)行加鎖和解鎖。沒(méi)關(guān)系,我知道你很懶,因此推薦個(gè)更簡(jiǎn)單的單行代碼解決方法,就是使用 std::lock_guard 類(lèi)。這個(gè)類(lèi)在創(chuàng)建時(shí)就鎖定了 mutex 對(duì)象,然后在結(jié)束時(shí)釋放。
繼續(xù)修改代碼:
void operator()() { for(int i = 0; i < 100000; i++) { lock_guard<mutex> lock(this->m); // The lock has been created now, and immediatly locks the mutex this->counter++; // This is the end of the for-loop scope, and the lock will be // destroyed, and in the destructor of the lock, it will // unlock the mutex } }
上面代碼已然是異常安全了,因?yàn)楫?dāng)異常發(fā)生時(shí),將會(huì)調(diào)用 lock 對(duì)象的析構(gòu)函數(shù),然后自動(dòng)進(jìn)行互斥量的解鎖。
記住,請(qǐng)使用放下代碼模板來(lái)編寫(xiě):
void long_function() { // some long code // Just a pair of curly braces { // Temp scope, create lock lock_guard<mutex> lock(this->m); // do some stuff // Close the scope, so the guard will unlock the mutex } }
上一篇:C語(yǔ)言實(shí)現(xiàn)BMP轉(zhuǎn)換JPG的方法
欄 目:C語(yǔ)言
下一篇:C語(yǔ)言之字符串模糊查詢(xún)方法的實(shí)現(xiàn)
本文標(biāo)題:C++多線程編程時(shí)的數(shù)據(jù)保護(hù)
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/2965.html
您可能感興趣的文章
- 04-02c語(yǔ)言沒(méi)有round函數(shù) round c語(yǔ)言
- 01-10如何判斷一個(gè)數(shù)是否為2的冪次方?若是,并判斷出來(lái)是多少次方
- 01-10深入理解C++中常見(jiàn)的關(guān)鍵字含義
- 01-10使用C++實(shí)現(xiàn)全排列算法的方法詳解
- 01-10如何判斷一個(gè)數(shù)是否為4的冪次方?若是,并判斷出來(lái)是多少次方
- 01-10c++中inline的用法分析
- 01-10用C++實(shí)現(xiàn)DBSCAN聚類(lèi)算法
- 01-10全排列算法的非遞歸實(shí)現(xiàn)與遞歸實(shí)現(xiàn)的方法(C++)
- 01-10C++大數(shù)模板(推薦)
- 01-10淺談C/C++中的static與extern關(guān)鍵字的使用詳解


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹(shù)的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問(wèn)題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 04-02c語(yǔ)言函數(shù)調(diào)用后清空內(nèi)存 c語(yǔ)言調(diào)用
- 04-02func函數(shù)+在C語(yǔ)言 func函數(shù)在c語(yǔ)言中
- 04-02c語(yǔ)言的正則匹配函數(shù) c語(yǔ)言正則表達(dá)
- 04-02c語(yǔ)言用函數(shù)寫(xiě)分段 用c語(yǔ)言表示分段
- 04-02c語(yǔ)言中對(duì)數(shù)函數(shù)的表達(dá)式 c語(yǔ)言中對(duì)
- 04-02c語(yǔ)言編寫(xiě)函數(shù)冒泡排序 c語(yǔ)言冒泡排
- 04-02c語(yǔ)言沒(méi)有round函數(shù) round c語(yǔ)言
- 04-02c語(yǔ)言分段函數(shù)怎么求 用c語(yǔ)言求分段
- 04-02C語(yǔ)言中怎么打出三角函數(shù) c語(yǔ)言中怎
- 04-02c語(yǔ)言調(diào)用函數(shù)求fibo C語(yǔ)言調(diào)用函數(shù)求
隨機(jī)閱讀
- 04-02jquery與jsp,用jquery
- 01-11Mac OSX 打開(kāi)原生自帶讀寫(xiě)NTFS功能(圖文
- 01-10delphi制作wav文件的方法
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 01-10SublimeText編譯C開(kāi)發(fā)環(huán)境設(shè)置
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-10C#中split用法實(shí)例總結(jié)
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載