C++多線程編程時的數據掩護。本站提示廣大學習愛好者:(C++多線程編程時的數據掩護)文章只能為提供參考,不一定能成為您想要的結果。以下是C++多線程編程時的數據掩護正文
在編寫多線程法式時,多個線程同時拜訪某個同享資本,會招致同步的成績,這篇文章中我們將引見 C++11 多線程編程中的數據掩護。
數據喪失
讓我們從一個簡略的例子開端,請看以下代碼:
#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; }
這個法式的目標就是數數,數到30萬,某些傻叉法式員想要優化數數的進程,是以創立了三個線程,應用一個同享變量 counter,每一個線程擔任給這個變量增長10萬計數。
這段代碼創立了一個名為 Incrementer 的類,該類包括一個公有變量 counter,其結構器異常簡略,只是將 counter 設置為 0.
緊接著是一個操作符重載,這意味著這個類的每一個實例都是被看成一個簡略函數來挪用的。普通我們挪用類的某個辦法時會如許 object.fooMethod(),但如今你現實上是直接挪用了對象,如object(). 由於我們是在操作符重載函數中將全部對象傳遞給了線程類。最初是一個 getCounter 辦法,前往 counter 變量的值。
再上去是法式的進口函數 main(),我們創立了三個線程,不外只創立了一個 Incrementer 類的實例,然後將這個實例傳遞給三個線程,留意這裡應用了 std::ref ,這相當因而傳遞了實例的援用對象,而不是對象的拷貝。
如今讓我們來看看法式履行的成果,假如這位傻叉法式員還夠聰慧的話,他會應用 GCC 4.7 或許更新版本,或許是 Clang 3.1 來停止編譯,編譯辦法:
g++ -std=c++11 -lpthread -o threading_example main.cpp
運轉成果:
[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
但等等,纰謬啊,法式並沒稀有數到30萬,有一次竟然只數到10萬,為何會如許呢?好吧,加1操尴尬刁難應現實的處置器指令其實包含:
movl counter(%rip), %eax addl $1, %eax movl %eax, counter(%rip)
首個指令將裝載 counter 的值到 %eax 存放器,緊接著存放器的值增1,然後將存放器的值移給內存中 counter 地點的地址。
我聽到你在嘀咕:這不錯,可為何會招致數數毛病的成績呢?嗯,還記得我們之前說過線程會同享處置器,由於只要單核。是以在某些點上,一個線程會按照指令履行完成,但在許多情形下,操作體系會對線程說:時光停止了,到前面列隊再來,然後別的一個線程開端履行,當下一個線程開端履行時,它會從被暫停的誰人地位開端履行。所以你猜會產生甚麼事,以後線程正預備履行存放器加1操作時,體系把處置器交給別的一個線程?
我真的不曉得會產生甚麼事,能夠我們在預備加1時,別的一個線程出去了,從新將 counter 值加載到存放器等多種情形的發生。誰也不曉得究竟產生了甚麼。
准確的做法
處理計劃就是請求統一個時光內只許可一個線程拜訪同享變量。這個可經由過程 std::mutex 類來處理。當線程進入時,加鎖、履行操作,然後釋放鎖。其他線程想要拜訪這個同享資本必需期待鎖釋放。
互斥(mutex) 是操作體系確保鎖息爭鎖操作是弗成朋分的。這意味著線程在對互斥量停止鎖息爭鎖的操作是不會被中止的。當線程對互斥量停止鎖或許解鎖時,該操作會在操作體系切換線程前完成。
而最好的工作是,當你試圖對互斥量停止加鎖操作時,其他的線程曾經鎖住了該互斥量,那你就必需期待直到其釋放。操作體系會跟蹤哪一個線程正在期待哪一個互斥量,被梗塞的線程會進入 "blocked onm" 狀況,意味著操作體系不會給這個梗塞的線程任何處置器時光,直到互斥量解鎖,是以也不會糟蹋 CPU 的輪回。假如有多個線程處於期待狀況,哪一個線程最早取得資本取決於操作體系自己,普通像 Windows 和 Linux 體系應用的是 FIFO 戰略,在及時操作體系中則是基於優先級的。
如今讓我們對下面的代碼停止改良:
#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 頭文件,增長了一個 m 的成員,類型是 mutex,在operator()() 中我們鎖住互斥量 m 然後對 counter 停止加1操作,然後釋放互斥量。
再次履行上述法式,成果以下:
[lucas@lucas-desktop src]$ ./threading_example 300000 [lucas@lucas-desktop src]$ ./threading_example 300000
這下數對了。不外在盤算機迷信中,沒有收費的午飯,應用互斥量會下降法式的機能,但這總比一個毛病的法式要強吧。
防備異常
當對變量停止加1操作時,是能夠會產生異常的,固然在我們這個例子中產生異常的機遇微不足道,然則在一些龐雜體系中是極有能夠的。下面的代碼其實不是異常平安的,當異常產生時,法式曾經停止了,可是互斥量照樣處於鎖的狀況。
為了確保互斥量在異常產生的情形下也能被解鎖,我們須要應用以下代碼:
for(int i = 0; i < 100000; i++) { this->m.lock(); try { this->counter++; this->m.unlock(); } catch(...) { this->m.unlock(); throw; } }
然則,這代碼太多了,而只是為了對互斥量停止加鎖息爭鎖。沒緊要,我曉得你很懶,是以推舉個更簡略的單行代碼處理辦法,就是應用 std::lock_guard 類。這個類在創立時就鎖定了 mutex 對象,然後在停止時釋放。
持續修正代碼:
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 } }
下面代碼已然是異常平安了,由於當異常產生時,將會挪用 lock 對象的析構函數,然後主動停止互斥量的解鎖。
記住,請應用放下代碼模板來編寫:
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 } }