C++多線程編程時(shí)的數(shù)據(jù)保護(hù)
在編寫多線程程序時(shí),多個線程同時(shí)訪問某個共享資源,會導(dǎo)致同步的問題,這篇文章中我們將介紹 C++11 多線程編程中的數(shù)據(jù)保護(hù)。
數(shù)據(jù)丟失
讓我們從一個簡單的例子開始,請看如下代碼:
#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;
}
這個程序的目的就是數(shù)數(shù),數(shù)到30萬,某些傻叉程序員想要優(yōu)化數(shù)數(shù)的過程,因此創(chuàng)建了三個線程,使用一個共享變量 counter,每個線程負(fù)責(zé)給這個變量增加10萬計(jì)數(shù)。
這段代碼創(chuàng)建了一個名為 Incrementer 的類,該類包含一個私有變量 counter,其構(gòu)造器非常簡單,只是將 counter 設(shè)置為 0.
緊接著是一個操作符重載,這意味著這個類的每個實(shí)例都是被當(dāng)作一個簡單函數(shù)來調(diào)用的。一般我們調(diào)用類的某個方法時(shí)會這樣 object.fooMethod(),但現(xiàn)在你實(shí)際上是直接調(diào)用了對象,如object(). 因?yàn)槲覀兪窃诓僮鞣剌d函數(shù)中將整個對象傳遞給了線程類。最后是一個 getCounter 方法,返回 counter 變量的值。
再下來是程序的入口函數(shù) main(),我們創(chuàng)建了三個線程,不過只創(chuàng)建了一個 Incrementer 類的實(shí)例,然后將這個實(shí)例傳遞給三個線程,注意這里使用了 std::ref ,這相當(dāng)于是傳遞了實(shí)例的引用對象,而不是對象的拷貝。
現(xiàn)在讓我們來看看程序執(zhí)行的結(jié)果,如果這位傻叉程序員還夠聰明的話,他會使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 來進(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
但等等,不對啊,程序并沒有數(shù)數(shù)到30萬,有一次居然只數(shù)到10萬,為什么會這樣呢?好吧,加1操作對應(yīng)實(shí)際的處理器指令其實(shí)包括:
movl counter(%rip), %eax addl $1, %eax movl %eax, counter(%rip)
首個指令將裝載 counter 的值到 %eax 寄存器,緊接著寄存器的值增1,然后將寄存器的值移給內(nèi)存中 counter 所在的地址。
我聽到你在嘀咕:這不錯,可為什么會導(dǎo)致數(shù)數(shù)錯誤的問題呢?嗯,還記得我們以前說過線程會共享處理器,因?yàn)橹挥袉魏恕R虼嗽谀承c(diǎn)上,一個線程會依照指令執(zhí)行完成,但在很多情況下,操作系統(tǒng)會對線程說:時(shí)間結(jié)束了,到后面排隊(duì)再來,然后另外一個線程開始執(zhí)行,當(dāng)下一個線程開始執(zhí)行時(shí),它會從被暫停的那個位置開始執(zhí)行。所以你猜會發(fā)生什么事,當(dāng)前線程正準(zhǔn)備執(zhí)行寄存器加1操作時(shí),系統(tǒng)把處理器交給另外一個線程?
我真的不知道會發(fā)生什么事,可能我們在準(zhǔn)備加1時(shí),另外一個線程進(jìn)來了,重新將 counter 值加載到寄存器等多種情況的產(chǎn)生。誰也不知道到底發(fā)生了什么。
正確的做法
解決方案就是要求同一個時(shí)間內(nèi)只允許一個線程訪問共享變量。這個可通過 std::mutex 類來解決。當(dāng)線程進(jìn)入時(shí),加鎖、執(zhí)行操作,然后釋放鎖。其他線程想要訪問這個共享資源必須等待鎖釋放。
互斥(mutex) 是操作系統(tǒng)確保鎖和解鎖操作是不可分割的。這意味著線程在對互斥量進(jìn)行鎖和解鎖的操作是不會被中斷的。當(dāng)線程對互斥量進(jìn)行鎖或者解鎖時(shí),該操作會在操作系統(tǒng)切換線程前完成。
而最好的事情是,當(dāng)你試圖對互斥量進(jìn)行加鎖操作時(shí),其他的線程已經(jīng)鎖住了該互斥量,那你就必須等待直到其釋放。操作系統(tǒng)會跟蹤哪個線程正在等待哪個互斥量,被堵塞的線程會進(jìn)入 "blocked onm" 狀態(tài),意味著操作系統(tǒng)不會給這個堵塞的線程任何處理器時(shí)間,直到互斥量解鎖,因此也不會浪費(fèi) CPU 的循環(huán)。如果有多個線程處于等待狀態(tài),哪個線程最先獲得資源取決于操作系統(tǒng)本身,一般像 Windows 和 Linux 系統(tǒng)使用的是 FIFO 策略,在實(shí)時(shí)操作系統(tǒng)中則是基于優(yōu)先級的。
現(xiàn)在讓我們對上面的代碼進(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 頭文件,增加了一個 m 的成員,類型是 mutex,在operator()() 中我們鎖住互斥量 m 然后對 counter 進(jìn)行加1操作,然后釋放互斥量。
再次執(zhí)行上述程序,結(jié)果如下:
[lucas@lucas-desktop src]$ ./threading_example 300000 [lucas@lucas-desktop src]$ ./threading_example 300000
這下數(shù)對了。不過在計(jì)算機(jī)科學(xué)中,沒有免費(fèi)的午餐,使用互斥量會降低程序的性能,但這總比一個錯誤的程序要強(qiáng)吧。
防范異常
當(dāng)對變量進(jìn)行加1操作時(shí),是可能會發(fā)生異常的,當(dāng)然在我們這個例子中發(fā)生異常的機(jī)會微乎其微,但是在一些復(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;
}
}
但是,這代碼太多了,而只是為了對互斥量進(jìn)行加鎖和解鎖。沒關(guān)系,我知道你很懶,因此推薦個更簡單的單行代碼解決方法,就是使用 std::lock_guard 類。這個類在創(chuàng)建時(shí)就鎖定了 mutex 對象,然后在結(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í),將會調(diào)用 lock 對象的析構(gòu)函數(shù),然后自動進(jìn)行互斥量的解鎖。
記住,請使用放下代碼模板來編寫:
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
}
}
相關(guān)文章
C語言?模擬實(shí)現(xiàn)memcpy與memmove函數(shù)詳解
這篇文章主要介紹了C語言詳解如何模擬內(nèi)存函數(shù),用到了mencpy與memmove兩個函數(shù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-04-04
C++基礎(chǔ)學(xué)習(xí)之利用兩個棧實(shí)現(xiàn)一個隊(duì)列
這篇文章主要給大家介紹了關(guān)于C++基礎(chǔ)學(xué)習(xí)之利用兩個棧實(shí)現(xiàn)一個隊(duì)列的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用C++具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
詳解C語言中symlink()函數(shù)和readlink()函數(shù)的使用
這篇文章主要介紹了詳解C語言中symlink()函數(shù)和readlink()函數(shù)的使用,是C語言入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09
C++逆向分析移除鏈表元素實(shí)現(xiàn)方法詳解
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(203.移除鏈表元素),本篇文章通過逆向分析的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2022-11-11
C++?ASIO實(shí)現(xiàn)異步套接字管理詳解
Boost?ASIO(Asynchronous?I/O)是一個用于異步I/O操作的C++庫,該框架提供了一種方便的方式來處理網(wǎng)絡(luò)通信、多線程編程和異步操作,本文介紹了如何通過ASIO框架實(shí)現(xiàn)一個簡單的異步網(wǎng)絡(luò)套接字應(yīng)用程序,需要的可以參考下2023-08-08

