C++ 11 atomic類型以及其memory_order介紹

C++ 11 atomic

簡介

Atomic?類型是c++11里面引入的一種類型栏账,它規(guī)定了當程序的多個線程同時訪問一個變量的時候應該遵循的規(guī)則(通過memory?order)谱轨。當訪問某個atomic類型的對象的時候通過指定std::memory_order可能會建立線程間同步以及對非atomic變量的內存訪問順序批旺。

std::atomic只可以用任何triviallyCopyable?的模板類型?T?實例化史辙,在頭文件里聲明菌仁,其原型有以下4中形式:

template<?class?T?>

struct?atomic;(1)

template<>

struct?atomic;(2)

template<>

struct?atomic;(3)

template<?class?T?>

struct?atomic;(4)

TriviallyCopyable:

特別注意的是當使用第一種形式的時候模板類型T必須是triviallyCopyable浩习,所謂triviallyCopyable必須滿足以下6點要求:

(1)每一個拷貝構造函數(copy?constructor)是trivial

(2)每一個移動構造函數(move?constructor)是trivial

(3)每個拷貝賦值表達式(copy?assignment?operator)是trivial

(4)每一個移動復制表達式(move?assignment?operator)是rivial

(5)至少有一個拷貝構造(copy?constructor)、移動拷貝(move?constructor)济丘、拷貝賦值表達式(copy?assignment)谱秽、移動賦值(move?assignment)是non-deleted

(6)析構函數是trivial

Trival?是甚麼意思:

Trival?Copy?Constructor,需要滿足以下6個條件才算是Trivial?Copy?Constructor

(1)拷貝構造函數不是程序員明確定義的(隱式的或是默認的)

(2)Class?T?沒有虛擬的成員函數(virtual?member?function)

(3)Class?T沒有虛擬基類

(4)Class?T?的直接基類的拷貝構造函數是trivial

(5)Class?T?的非靜態(tài)類成員變量的拷貝構造函數是trivial

(6)Class?T?沒有非靜態(tài)的volatile-qualified類型的成員變量

Example:

#include?

#include?

#include?

class?B

{

public:

virtual?void?fun()?{}

};

class?A

{

public:

A()?noexcept?{}

A(const?A&?other)?:?value_(other.value_)?{}

//virtual?void?fun()?{}//compile?error?because?of?virtual?member?function

int?value_;

B?b;//compile?error?because?of?virtual?member?function?of?B

};

class?C

{

public:

virtual?void?func()?{}

};

class?D

{

public:

virtual?void?fun()?{}

};

class?E?:?public?D

};

int?main()

{

std::atomic?a;

std::atomic_bool?boolValue;

std::atomic?c;

A?aa;

aa.value_?=?3;

a.store(aa);

std::cout<<"whther?class?A?is?trivial?type?"<::value<

std::cout<<"whther?class?B?is?trivial?type?"<::value<

std::cout<<"whther?class?B?is?trivial?type?"<::value<

std::cout<<"whther?class?B?is?trivial?type?"<::value<

std::cout<<"whther?class?B?is?trivial?type?"<::value<

std::cout<<"value_?is?"<

return?0;

}

如上面的程序說明摹迷,當class?A里面定義了virtual?member?function的時候疟赊,編譯的時候會報錯,另外可以用std::is_trivial來判斷類型是否是trivial的峡碉,如果是trivial則其alue值為true否則為false近哟。如上程序在gcc?4.9?c++11下面編譯通過,輸出為:

whether?class?A?is?trivial?type?0

whether?class?B?is?trivial?type?1

whether?class?C?is?trivial?type?0

whether?class?D?is?trivial?type?0

whether?class?E?is?trivial?type?0

value_?is?3

這里我有個疑問鲫寄,因為我的例子中class?A并非Trivial但是還是可以實例化atomic結構吉执?

memory_order

對于atomic對象操作有6種memory?ordering選項,memory_order_relaxed地来、

memory_order_consume鼠证、memory_order_acquire、memory_order_release靠抑、memory_order_acq_rel和memory_order_seq_cst量九;默認情況下的為memory_order_seq_cst。盡管有6種選項,但是它們代表三種模型:sequentially-consistent?ordering(memory_order_seq_cst)荠列、acquire-release?ordering(memory_order_consume,memory_order_acquire,memory_order_release,?and?memory_order_acq_rel)类浪、relaxed?ordering?(memory_order_relaxed)。

另外需要注意的是對于不同的memory?ordering運行在不同的cpu架構的機器上運行的代價是不一樣的肌似,比如對于對同步指令的需求sequentially-consistent?ordering模型大于acquire-release?ordering或者relaxed?ordering费就,acquire-release?ordering大于relaxed?ordering;如果是運行在多處理器的操作系統上面川队,這些額外的同步指令開銷可能會消耗重要的cpu時間力细,從而造成總體系統性能的下降。對于x86或x86-64架構的處理器在使用acquire-release模型的時候不需要任何額外的指令開銷固额,甚至是對于比較嚴格的sequentially?consisten?ordering也不需要特殊處理眠蚂,并且花費的代價也很少。

sequentially?consistent?ordering

Sequentially?consistent?ordering是atomic默認的操作選項斗躏,由于它暗含程序的行為對于atomic變量操作的一致性逝慧。如果atomic變量的所有操作順序都確定了,那麼多線程程序行為就像在單線程內以特定的順序執(zhí)行這些操作即所有線程可以看到相同的操作執(zhí)行順序這也意味著操作不可以被reorder啄糙。通俗來說笛臣,如果1線程執(zhí)行了A,B兩個操作隧饼,然后2線程執(zhí)行B操作沈堡,那麼此時2線程必定知道A操作被執(zhí)行過了,因為這個操作順序(A在B之前執(zhí)行)是所有線程都能夠看到的燕雁。比如下面的程序代碼:

#include?

#include?

#include?

std::atomic?x,y;

std::atomic?z;

void?write_x()

{

x.store(true,std::memory_order_seq_cst);??????#1

}

void?write_y()

{

y.store(true,std::memory_order_seq_cst);??????#2

}

void?read_x_then_y()

{

while(!x.load(std::memory_order_seq_cst));

if(y.load(std::memory_order_seq_cst))?????????#3

++z;

}

void?read_y_then_x()

{

while(!y.load(std::memory_order_seq_cst));

if(x.load(std::memory_order_seq_cst))?????????#4

++z;

}

int?main()

{

x=false;

y=false;

z=0;

std::thread?a(write_x);

std::thread?b(write_y);

std::thread?c(read_x_then_y);

std::thread?d(read_y_then_x);

a.join();

b.join();

c.join();

d.join();

assert(z.load()!=0);??????????????????????????#5

}

例子中的assert#5永遠不可能發(fā)生诞丽,因為store?to?x和store?to?y肯定是首先發(fā)生(發(fā)生先后順序未知),可以假設在read_x_then_y中?y.load為false贵白,那麼可以確定x此時為true率拒,也就是說sotre?to?x先于store?y發(fā)生。那麼雜read_y_then_x中禁荒,x.load一定為true即++z肯定會被執(zhí)行猬膨。這是因為由于操作一致性能被所有線程看見,那麼當y變?yōu)閠rue(跳出循環(huán))即sotre?to?y發(fā)生呛伴,由于store?to?x先于sotre?to?y所以此時d線程也看到此時x為true了勃痴。Sequentially?consistent?ordering是最直觀的順序,但也是最昂貴的操作热康。因為它需要對所有的線程進行同步沛申,在一個多處理器的操作系統中,這些操作會消耗額外的時間用來處理器之間的通信姐军。


relaxed?ordering

relaxed?ordering不會造成順序同步铁材,在單個線程類依然是遵循操作在前先執(zhí)行的順序尖淘,但是到其他線程里面則無法知道這種先后順序即在1線程內A操作先于B操作,那麼在2線程里面執(zhí)行B操作的時候它可能會認為A操作在此之前并沒有被執(zhí)行過著觉。依然來看一個例子:

#include?

#include?

#include?

std::atomic?x,y;

std::atomic?z;

void?write_x_then_y()

{

x.store(true,std::memory_order_relaxed);????#1

y.store(true,std::memory_order_relaxed);????#2

}

void?read_y_then_x()

{

while(!y.load(std::memory_order_relaxed));??#3

if(x.load(std::memory_order_relaxed))???????#4

++z;

}

int?main()

{

x=false;

y=false;

z=0;

std::thread?a(write_x_then_y);

std::thread?b(read_y_then_x);

a.join();

b.join();

assert(z.load()!=0);????????????????????????#5

}

在這個例子中村生,assert?#5有可能發(fā)生,當在線程a里面兩個操作都執(zhí)行完之后饼丘,切換到線程b趁桃,當跳出循環(huán)判斷x的時候,由于線程a內的兩個賦值操作的順序線程b并不知道肄鸽,所以可能認為x仍然為false卫病,從而最終被assert捕獲。


為了更好的說明問題典徘,下面用一個比較復雜的例子:

#include?

#include?

#include?

std::atomic?x(0),y(0),z(0);???????????????????????????????????#1

std::atomic?go(false);???????????????????????????????????????#2

unsigned?const?loop_count=10;

struct?read_values

{

int?x,y,z;

};

read_values?values1[loop_count];

read_values?values2[loop_count];

read_values?values3[loop_count];

read_values?values4[loop_count];

read_values?values5[loop_count];

void?increment(std::atomic*?var_to_inc,read_values*?values)

{

while(!go)???????????????????????????????????????????????????????????#3

std::this_thread::yield();

for(unsigned?i=0;i

{

values[i].x=x.load(std::memory_order_relaxed);

values[i].y=y.load(std::memory_order_relaxed);

values[i].z=z.load(std::memory_order_relaxed);

var_to_inc->store(i+1,std::memory_order_relaxed);?????????????#4

std::this_thread::yield();

}

}

void?read_vals(read_values*?values)

{

while(!go)???????????????????????????????????????????????????????????#5

std::this_thread::yield();

for(unsigned?i=0;i

{

values[i].x=x.load(std::memory_order_relaxed);

values[i].y=y.load(std::memory_order_relaxed);

values[i].z=z.load(std::memory_order_relaxed);

std::this_thread::yield();

}

}

void?print(read_values*?v)

{

for(unsigned?i=0;i

{

if(i)

std::cout<<",";

std::cout<<"("<

}

std::cout<

}

int?main()

{

std::thread?t1(increment,&x,values1);

std::thread?t2(increment,&y,values2);

std::thread?t3(increment,&z,values3);

std::thread?t4(read_vals,values4);

std::thread?t5(read_vals,values5);

go=true;?????????????????????????????????????????????????????????????#6

t5.join();

t4.join();

t3.join();

t2.join();

t1.join();

print(values1);??????????????????????????????????????????????????????#7

print(values2);

print(values3);

print(values4);

print(values5);

}

#3?Spin,?waitingfor?the?signal

#5?Spin,?waiting?for?the?signal

#6?Signals?to?start?execution?of?main?loop

#7?Prints?the?final?values

三個共享的全局變量蟀苛,5個線程。其中前三個線程讀取全局變量值并且依次增加x,y,z的值烂斋。后面兩個線程讀取三個全局變量的值屹逛。程序的輸出可能為:

(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),(9,9,10)

(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),(10,9,10)

(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),(0,0,9)

(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),(9,10,10),(10,10,10)

(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),(8,8,9)

從輸出上可以看到:

(1)第一行的x础废,第二行的y汛骂,第三行的z都是依次遞增上去的(因為對于單個賦值操作是在同一個線程中執(zhí)行)。

(2)元素x评腺,y帘瞭,z都是遞增的(不可能出現減少的情況),雖然遞增速度隨機蒿讥。

(3)線程3中蝶念,x和y都沒有更新,但實際上線程1芋绸,2都沒有停止媒殉。

由于不同線程間沒有固定的操作順序,所以全局變量的讀取具有隨機性(取決與該線程內該變量甚麼時候更新)摔敛,由于變量的值一直都是遞增增加廷蓉,所以更新的值也是遞增的不可能出現在時刻i時x=5,到時刻y時x=3(y?>?x)的情況出現马昙。有一個比較形象的比喻解釋這個現象桃犬,可以打開本文的參考鏈接2中的Understanding?Relaxed?Ordering

從上面的例子中可以看出用memory_order_relaxed選項是很難處理預測的,它必須結合另外一些有更強的同步性選項才能使用行楞,一般不建議用這個選項除非你確定一定要用這個選項攒暇。

acquire-release

Acquire-release?ordering,它和relaxed?ordering一樣同樣沒有全局性的操作順序子房,但它引入了另外一些同步形用。在這種模式下atomic變量的load操作是acquire(memory_order_acquire)的就轧,而變量的store是release(memory_order_release)的。C++規(guī)定release操作會同步到操作acquire操作田度。這就意味著不同線程仍然只能看到不同的操作順序钓丰,但是這些順序有更嚴格的限制了。下面仍然用例子進行說明:

#include?

#include?

#include?

std::atomic?x,y;

std::atomic?z;

void?write_x()

{

x.store(true,std::memory_order_release);

}

void?write_y()

{

y.store(true,std::memory_order_release);

}

void?read_x_then_y()

{

while(!x.load(std::memory_order_acquire));

if(y.load(std::memory_order_acquire))??????????????#1

++z;

}

void?read_y_then_x()

{

while(!y.load(std::memory_order_acquire));

if(x.load(std::memory_order_acquire))??????????????#2

++z;

}

int?main()

{

x=false;

y=false;

z=0;

std::thread?a(write_x);

std::thread?b(write_y);

std::thread?c(read_x_then_y);

std::thread?d(read_y_then_x);

a.join();

b.join();

c.join();

d.join();

assert(z.load()!=0);???????????????????????????????#3

}

這個例子中assert(#3)可能會發(fā)生每币,因為對x携丁,y的寫操作是在不同的線程中執(zhí)行的所以x,y的同步操作是互不影響的


所以為了讓assert不發(fā)生兰怠,可以將兩個變量的寫操作放到同一個線程中:

#include?

#include

#include?

std::atomic?x,y;

std::atomic?z;

void?write_x_then_y()

{

x.store(true,std::memory_order_relaxed);?????????????#1

y.store(true,std::memory_order_release);?????????????#2

}

void?read_y_then_x()

{

while(!y.load(std::memory_order_acquire));???????????#3

if(x.load(std::memory_order_relaxed))????????????????#4

++z;

}

int?main()

{

x=false;

y=false;

z=0;

std::thread?a(write_x_then_y);

std::thread?b(read_y_then_x);

a.join();

b.join();

assert(z.load()!=0);?????????????????????????????????#5

}

#1?Spin,?waiting?for?y?to?be?set?to?true

此時即可保證y.load返回true后梦鉴,x.load一定也是true

Transitive-Synchronization?with?acquire-release?ordering

同步的可傳遞性,即不同線程間的同步具有可傳遞的特性揭保。同樣上例子:

std::atomic?data[5];

std::atomic?sync1(false),sync2(false);

void?thread_1()

{

data[0].store(42,std::memory_order_relaxed);

data[1].store(97,std::memory_order_relaxed);

data[2].store(17,std::memory_order_relaxed);

data[3].store(-141,std::memory_order_relaxed);

data[4].store(2003,std::memory_order_relaxed);

sync1.store(true,std::memory_order_release);????????????????#1

}

void?thread_2()

{

while(!sync1.load(std::memory_order_acquire));??????????????#2

sync2.store(true,std::memory_order_release);????????????????#3

}

void?thread_3()

{

while(!sync2.load(std::memory_order_acquire));??????????????#4

assert(data[0].load(std::memory_order_relaxed)==42);

assert(data[1].load(std::memory_order_relaxed)==97);

assert(data[2].load(std::memory_order_relaxed)==17);

assert(data[3].load(std::memory_order_relaxed)==-141);

assert(data[4].load(std::memory_order_relaxed)==2003);

}

#1?Set?sync1

#2?Loop?until?sync1?is?set

#3?Set?sync2

#4?Loop?until?sync2?is?set

如以上一段程序肥橙,第一個線程以relaxed?orderin寫數據到data全局變量中,然后以release方式將數據寫如sync1變量中秸侣。第二個線程先等待sync1變量變?yōu)閠rue存筏,然后在以release方式寫數據到sync2變量中。第三個線程則等待sync2被同步變?yōu)閠rue味榛,然后assert捕獲椭坚。這個例子的輸出assert將永遠都不會發(fā)生,這是因為sync1.store發(fā)生在sync1.load之前搏色,sync1.load發(fā)生在sync2.store之前善茎,而sync2.store發(fā)生在sync2.load之前,所以在執(zhí)行l(wèi)oad?data數組的數據的時候實際上就保證了频轿,data.store發(fā)生在data.load之前了盡管data.store是以relaxed?oredering的方式操作的垂涯。

參考資料:

http://en.cppreference.com/w/cpp/atomic/atomic

www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市航邢,隨后出現的幾起案子耕赘,更是在濱河造成了極大的恐慌,老刑警劉巖膳殷,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件操骡,死亡現場離奇詭異,居然都是意外死亡秽之,警方通過查閱死者的電腦和手機当娱,發(fā)現死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來考榨,“玉大人跨细,你說我怎么就攤上這事『又剩” “怎么了冀惭?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵震叙,是天一觀的道長。 經常有香客問我散休,道長媒楼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任戚丸,我火速辦了婚禮划址,結果婚禮上,老公的妹妹穿的比我還像新娘限府。我一直安慰自己夺颤,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布胁勺。 她就那樣靜靜地躺著世澜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪署穗。 梳的紋絲不亂的頭發(fā)上寥裂,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音案疲,去河邊找鬼封恰。 笑死,一個胖子當著我的面吹牛络拌,可吹牛的內容都是我干的俭驮。 我是一名探鬼主播回溺,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼春贸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遗遵?” 一聲冷哼從身側響起萍恕,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎车要,沒想到半個月后允粤,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡翼岁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年类垫,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琅坡。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡悉患,死狀恐怖,靈堂內的尸體忽然破棺而出榆俺,到底是詐尸還是另有隱情售躁,我是刑警寧澤坞淮,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站陪捷,受9級特大地震影響回窘,放射性物質發(fā)生泄漏。R本人自食惡果不足惜市袖,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一啡直、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧苍碟,春花似錦付枫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至县忌,卻和暖如春掂榔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背症杏。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工装获, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厉颤。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓穴豫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逼友。 傳聞我的和親對象是個殘疾皇子精肃,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內容

  • 接著上節(jié) mutex,本節(jié)主要介紹atomic的內容帜乞,練習代碼地址司抱。本文參考http://www.cplusplu...
    jorion閱讀 73,584評論 1 14
  • 不講語言特性,只從工程角度出發(fā)黎烈,個人覺得C++標準委員會在C++11中對多線程庫的引入是有史以來做得最人道的一件事...
    stidio閱讀 13,244評論 0 11
  • 接著上節(jié) condition_varible 习柠,本節(jié)主要介紹future的內容,練習代碼地址照棋。本文參考http:/...
    jorion閱讀 14,761評論 1 5
  • 作者:晴妞Q媽 1. 話說资溃,補課班調整上課時間,晴妞這周日有了一整天的休息時間烈炭。 Q媽和Q爸商量著溶锭,買點東西去看望...
    晴妞Q媽閱讀 310評論 0 3
  • Nginx rewrite基本語法 Nginx的rewrite語法其實很簡單.用到的指令無非是這幾個: set i...
    Zhaifg閱讀 14,495評論 2 21