詳解C++11原子類型與原子操作
1.認(rèn)識原子操作
原子操作就是在多線程程序中“最小的且不可并行化的”操作,意味著多個(gè)線程訪問同一個(gè)資源時(shí),有且僅有一個(gè)線程能對資源進(jìn)行操作。通常情況下原子操作可以通過互斥的訪問方式來保證,例如Linux下的互斥鎖(mutex),Windows下的臨界區(qū)(Critical Section)等。下面看一個(gè)Linux環(huán)境使用POSIX標(biāo)準(zhǔn)的pthread庫實(shí)現(xiàn)多線程下的原子操作:
#include <pthread.h>
#include <iostream>
using namespace std;
int64_t total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;
//線程函數(shù),用于累加
void* threadFunc(void* args)
{
int64_t endNum=*(int64_t*)args;
for(int64_t i=1;i<=endNum;++i)
{
pthread_mutex_lock(&m);
total+=i;
pthread_mutex_unlock(&m);
}
}
int main()
{
int64_t endNum=100;
pthread_t thread1ID=0,thread2ID=0;
//創(chuàng)建線程1
pthread_create(&thread1ID,NULL,threadFunc,&endNum);
//創(chuàng)建線程2
pthread_create(&thread2ID,NULL,threadFunc,&endNum);
//阻塞等待線程1結(jié)束并回收資源
pthread_join(thread1ID,NULL);
//阻塞等待線程2結(jié)束并回收資源
pthread_join(thread2ID,NULL);
cout<<"total="<<total<<endl; //10100
}
上面的代碼,兩個(gè)線程同時(shí)對total進(jìn)行操作,為了保證total+=i 的原子性,采用互斥鎖來保證同一時(shí)刻只有同一線程執(zhí)行total+=i操作,所以得出正確結(jié)果total=10100。如果沒有做互斥處理,那么total同一時(shí)刻可能會(huì)被兩個(gè)線程同時(shí)操作,即會(huì)出現(xiàn)兩個(gè)線程同時(shí)讀取了寄存器中的total值,分別操作之后又寫入寄存器,這樣就會(huì)有一個(gè)線程的增加操作無效,會(huì)得出一個(gè)小于10100隨機(jī)的錯(cuò)誤值。
2.C++11實(shí)現(xiàn)原子操作
在C++11之前,使用第三方API可以實(shí)現(xiàn)并行編程,比如pthread多線程庫,但是在使用時(shí)需要?jiǎng)?chuàng)建互斥鎖,以及進(jìn)行加鎖、解鎖等操作來保證多線程對臨界資源的原子操作,這無疑增加了開發(fā)的工作量。不過從C++11開始,C++從語言層面開始支持并行編程,內(nèi)容包括了管理線程、保護(hù)共享數(shù)據(jù)、線程間的同步操作、低級原子操作等各種類。新標(biāo)準(zhǔn)極大地提高了程序的可移植性,以前的多線程依賴于具體的平臺(tái),而現(xiàn)在有了統(tǒng)一的接口。
C++11通過引入原子類型幫助開發(fā)者輕松實(shí)現(xiàn)原子操作。
#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
atomic_int64_t total = 0; //atomic_int64_t相當(dāng)于int64_t,但是本身就擁有原子性
//線程函數(shù),用于累加
void threadFunc(int64_t endNum)
{
for (int64_t i = 1; i <= endNum; ++i)
{
total += i;
}
}
int main()
{
int64_t endNum = 100;
thread t1(threadFunc, endNum);
thread t2(threadFunc, endNum);
t1.join();
t2.join();
cout << "total=" << total << endl; //10100
}
程序正常編譯并運(yùn)行輸出正確結(jié)果total=10100。使用C++11提供的原子類型與多線程標(biāo)準(zhǔn)接口,簡潔地實(shí)現(xiàn)了多線程對臨界資源的原子操作。原子類型C++11中通過atomic<T>類模板來定義,比如atomic_int64_t是通過typedef atomic<int64_t> atomic_int64_t實(shí)現(xiàn)的,使用時(shí)需包含頭文件<atomic>。除了提供atomic_int64_t,還提供了其它的原子類型。常見的原子類型有
|
原子類型名稱 |
對應(yīng)內(nèi)置類型 |
|---|---|
|
atomic_bool |
bool |
|
atomic_char |
atomic_char |
|
atomic_char |
signed char |
|
atomic_uchar |
unsigned char |
|
atomic_short |
short |
|
atomic_ushort |
unsigned short |
|
atomic_int |
int |
|
atomic_uint |
unsigned int |
|
atomic_long |
long |
|
atomic_ulong |
unsigned long |
|
atomic_llong |
long long |
|
atomic_ullong |
unsigned long long |
|
atomic_ullong |
unsigned long long |
|
atomic_char16_t |
char16_t |
|
atomic_char32_t |
char32_t |
|
atomic_wchar_t |
wchar_t |
原子操作是平臺(tái)相關(guān)的,原子類型能夠?qū)崿F(xiàn)原子操作是因?yàn)镃++11對原子類型的操作進(jìn)行了抽象,定義了統(tǒng)一的接口,并要求編譯器產(chǎn)生平臺(tái)相關(guān)的原子操作的具體實(shí)現(xiàn)。C++11標(biāo)準(zhǔn)將原子操作定義為atomic模板類的成員函數(shù),包括讀(load)、寫(store)、交換(exchange)等。對于內(nèi)置類型而言,主要是通過重載一些全局操作符來完成的。比如對上文total+=i的原子加操作,是通過對operator+=重載來實(shí)現(xiàn)的。使用g++編譯的話,在x86_64的機(jī)器上,operator+=()函數(shù)會(huì)產(chǎn)生一條特殊的以lock為前綴的x86_64指令,用于控制總線及實(shí)現(xiàn)x86_64平臺(tái)上的原子性加法。
有一個(gè)比較特殊的原子類型是atomic_flag,因?yàn)閍tomic_flag與其他原子類型不同,它是無鎖(lock_free)的,即線程對其訪問不需要加鎖,而其他的原子類型不一定是無鎖的。因?yàn)閍tomic<T>并不能保證類型T是無鎖的,另外不同平臺(tái)的處理器處理方式不同,也不能保證必定無鎖,所以其他的類型都會(huì)有is_lock_free()成員函數(shù)來判斷是否是無鎖的。atomic_flag只支持test_and_set()以及clear()兩個(gè)成員函數(shù),test_and_set()函數(shù)檢查 std::atomic_flag 標(biāo)志,如果 std::atomic_flag 之前沒有被設(shè)置過,則設(shè)置 std::atomic_flag 的標(biāo)志;如果之前 std::atomic_flag 已被設(shè)置,則返回 true,否則返回 false。clear()函數(shù)清除 std::atomic_flag 標(biāo)志使得下一次調(diào)用 std::atomic_flag::test_and_set()返回 false??梢杂胊tomic_flag的成員函數(shù)test_and_set()和clear()來實(shí)現(xiàn)一個(gè)自旋鎖(spin lock):
#include <unistd.h>
#include <atomic>
#include <thread>
#include <iostream>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void func1()
{
while (lock.test_and_set(std::memory_order_acquire)) // 在主線程中設(shè)置為true,需要等待t2線程clear
{
std::cout << "func1 wait" << std::endl;
}
std::cout << "func1 do something" << std::endl;
}
void func2()
{
std::cout << "func2 start" << std::endl;
lock.clear();
}
int main()
{
lock.test_and_set(); // 設(shè)置狀態(tài)
std::thread t1(func1);
usleep(1); //睡眠1us
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}
以上代碼中,定義了一個(gè)atomic_flag對象lock,使用初始值A(chǔ)TOMIC_FLAG_INIT進(jìn)行初始化,即處于false的狀態(tài)。線程t1調(diào)用test_and_set()一直返回true(因?yàn)樵谥骶€程中被設(shè)置過),所以一直在等待,而等待一段時(shí)間后當(dāng)線程t2運(yùn)行并調(diào)用了clear(),test_and_set()返回了false退出循環(huán)等待并進(jìn)行相應(yīng)操作。這樣一來,就實(shí)現(xiàn)了一個(gè)線程等待另一個(gè)線程的效果。當(dāng)然,可以封裝成鎖操作的方式,比如:
void Lock(atomic_flag& lock){ while ( lock.test_and_set()); }
void UnLock(atomic_flag& lock){ lock.clear(); }
這樣一來,就可以通過Lock()和UnLock()的方式來互斥地訪問臨界區(qū)。
以上就是詳解C++11原子類型與原子操作的詳細(xì)內(nèi)容,更多關(guān)于C++11原子類型與原子操作的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C++中遞增運(yùn)算符重載的實(shí)現(xiàn)
本文主要詳解運(yùn)算符重載里的遞增運(yùn)算符重載;遞增和遞減原理是一樣的,這里就只分享遞增的重載;提到遞增遞減,我們都知道又前置和后置兩種方法, 那今天就詳解一下前置遞增和后置遞增的細(xì)節(jié),拿捏遞增運(yùn)算符重載2022-06-06
C/C++?Qt數(shù)據(jù)庫與SqlTableModel組件應(yīng)用教程
SqlTableModel?組件可以將數(shù)據(jù)庫中的特定字段動(dòng)態(tài)顯示在TableView表格組件中,這篇文章將主要介紹SqlTableModel組件一些常用的操作,需要的朋友可以參考一下2021-12-12
C++的sstream標(biāo)準(zhǔn)庫詳細(xì)介紹
以下是對C++中的的sstream標(biāo)準(zhǔn)庫進(jìn)行了詳細(xì)的介紹,需要的朋友可以過來參考下2013-09-09
C++實(shí)現(xiàn)list增刪查改模擬的示例代碼
本文主要介紹了C++實(shí)現(xiàn)list增刪查改模擬,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-12-12

