日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁 > 單片機(jī) > 程序喵大人
這篇文章簡單介紹下并發(fā)編程中一個(gè)重要的知識點(diǎn):內(nèi)存模型。直接看這段代碼:
#include #include int x = 0;int y = 0; void func1() {  x = 100;  y = 2;} void func2() {  while (y == 2) {  std::cout << x << std::endl;  break;  }} int main() {  std::thread t1(func1);  std::thread t2(func2);  t1.join();  t2.join();}
大家猜一猜這段代碼會(huì)輸出什么?100嗎?絕大數(shù)時(shí)候會(huì)是100,但是也有極小概率會(huì)是0,理論上有輸出0的可能。這里涉及到內(nèi)存序(memory order)的知識點(diǎn),在這之前,需要先了解下什么是改動(dòng)序列。在一個(gè)C++程序中,每個(gè)對象都具有一個(gè)改動(dòng)隊(duì)列,它由所有線程在對象上的全部寫操作構(gòu)成,變量的值會(huì)隨著時(shí)間推移形成一個(gè)序列,不同線程觀察同一個(gè)變量的序列,正常情況下是一致的,如果出現(xiàn)不一致,就說明出現(xiàn)了數(shù)據(jù)競爭線程不安全的問題。看圖,隨著時(shí)間的推移,一個(gè)變量可能做了圖中的改動(dòng),產(chǎn)生了一個(gè)改動(dòng)序列,即(1,2,3,4,5,6,7,8,9,10),然而理論上來說,不同線程不能保證他們看見的是最新的值,比如同一時(shí)刻,線程a可能看見的是5,線程b可能看見的是4,線程c可能看見的是3,d是4,e是2。然后過了一段時(shí)間,可能變成了a(10),b(4),c(8),d(6),e(3)。每個(gè)線程看到的只會(huì)是序列中上一次看到的之后的值,不可能是之前的,時(shí)光不能倒流。
同理,如圖:如果有兩個(gè)變量,它們的改動(dòng)序列如圖,然而同一時(shí)刻,理論上可能不同線程看到的值不同。再回到上面那段代碼:
#include #include int x = 0;int y = 0;void func1() { x = 100; y = 2;}void func2() { while (y == 2) { std::cout << x << std::endl; break; }}int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join();}
t1和t2線程看到的x和y值可能有(0,0)(0,2)(100,0)(100,2),所以上面的代碼運(yùn)行時(shí),正常會(huì)輸出100,但是也有可能會(huì)輸出0。你可能會(huì)說,可得了吧,我測試了幾十萬次,輸出的都是100,從沒出現(xiàn)過0。是的,大概率都是100,這種現(xiàn)象只有理論上會(huì)出現(xiàn),而且估計(jì)意外只會(huì)在內(nèi)存序相對寬松的Arm機(jī)器上會(huì)出現(xiàn),正常的X86應(yīng)該不會(huì)出現(xiàn)這種問題。但我們寫代碼還是要往標(biāo)準(zhǔn)了寫。為什么會(huì)出現(xiàn)這種現(xiàn)象?因?yàn)榫幾g器有指令亂序的優(yōu)化。還是上面那段代碼:
int x = 0;int y = 0;void func() { x = 100; y = 2;}
函數(shù)func()中,按順序來看可能是先執(zhí)行x = 100再執(zhí)行y = 2,但實(shí)際情況可能不同,有可能編譯器會(huì)做一些指令重排序的優(yōu)化,真正優(yōu)化后的結(jié)果可能會(huì)是y = 2,再x = 100,重排序后再運(yùn)行,結(jié)果和順序執(zhí)行完全相同。(你可能會(huì)問,為什么要做這種優(yōu)化,先執(zhí)行誰后執(zhí)行誰都需要執(zhí)行,有意義嗎?文中我只是舉一個(gè)比較簡單的例子,可能這里沒有意義,但遇到真正復(fù)雜的代碼時(shí)指令重排序還是很有效的優(yōu)化策略,其實(shí)如果你學(xué)過計(jì)算機(jī)體系結(jié)構(gòu)就會(huì)知道,這種沒有任何依賴關(guān)系的指令是可以做并行優(yōu)化的,具體是什么術(shù)語我也記不起來了,好像是SIMD。)編譯器只會(huì)保證單線程環(huán)境下,優(yōu)化執(zhí)行的最終結(jié)果是一致的,所以這種優(yōu)化就會(huì)導(dǎo)致多線程情況下的數(shù)據(jù)沖突問題,比如上面的代碼:
void func1() { x = 100; y = 2;}void func2() { while (y == 2) { std::cout << x << std::endl; break; }}int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join();}
由于執(zhí)行重排序的原因,無法保證另一個(gè)線程在執(zhí)行func2的時(shí)候,x和y的賦值順序,所以上面x的輸出,有可能是0,也有可能是100。這也就是為什么會(huì)出現(xiàn)上面介紹的改動(dòng)序列的原因。那怎么解決這種問題?肯定是要在某些情況下,禁止這種指令重排序。可以引入原子操作,我們可以把上面的x和y的定義改為:
std::atomic<int> x = 0;std::atomic<int> y = 0;
結(jié)果自然而然就會(huì)變得正常。為什么?因?yàn)镃++的atomic不僅僅是原子操作,它很重要的一點(diǎn)是可以禁止這種指令重排序。我們平時(shí)使用atomic可能都是這樣使用:
int value = x.load();x.store(100); 
但其實(shí)atomic的多數(shù)函數(shù)都是重載函數(shù),它可以配置一些參數(shù),這些參數(shù)就是內(nèi)存序的類型參數(shù):
x.store(100, std::memory_order_relaxed);
C++里關(guān)于一共引入了6種內(nèi)存序的類型:
  • memory_order_relaxexd:只有普通的原子性,沒有任何內(nèi)存次序的要求。
  • memory_order_seq_cst:與代碼順序嚴(yán)格一致。
  • memory_order_acquire:載入語義,當(dāng)前線程,load操作之后的讀寫操作不能被重排序到當(dāng)前指令前面。如果其它線程對此變量使用release的store操作,在當(dāng)前線程是可見的。
  • memory_order_release:存儲(chǔ)語義,當(dāng)前線程,store操作之前的讀寫操作不能重排序到當(dāng)前指令后面,如果其它線程對此變量使用了acquire的load操作,當(dāng)前線程store之前的任何讀寫操作都對其它線程可見。
  • memory_order_acq_rel:它等于acquire + release
  • memory_order_consume:C++17中明確建議我們不使用此次序,以后會(huì)被廢棄掉,咱也就不糾結(jié)它了。
盡管有6種內(nèi)存序,但其實(shí)可簡單劃分為3種模式
  • 先后一致次序(Sequential Consistency Ordering):這就是atomic默認(rèn)的內(nèi)存次序,它是最直觀、最符合直覺的內(nèi)存次序,所有關(guān)于此次序的實(shí)例,都嚴(yán)格保持先后順序,這種內(nèi)存模型無法重新編排次序,它要求在所有線程間進(jìn)行全局同步,因此也是代價(jià)最高的內(nèi)存次序。
  • 寬松次序(Relaxed Ordering):你可以理解為使用搭配這種次序的atomic,只有原子性,而對內(nèi)存次序沒有任何要求,指令重排序之類的優(yōu)化還是正常進(jìn)行。
  • 獲取-釋放次序(Acquire-Release Ordering):它比寬松次序嚴(yán)格一些,卻沒有先后一致次序那樣特別嚴(yán)格。在此次序模型中,載入(load)操作可以使用memory_order_acquire語義,存儲(chǔ)(store)可以使用memory_order_release語義,而讀-改-寫(fetch_add、exchange)可以使用memory_order_acq_rel語義。
所以,我們看下這段使用relaxed模型的代碼會(huì)不會(huì)觸發(fā)assert:
#include #include #include  std::atomic<bool> x, y;std::atomic<int> z; void write_x_then_y() {  x.store(true, std::memory_order_relaxed);  y.store(true, std::memory_order_relaxed); }  void read_y_then_x() {  while (!y.load(std::memory_order_relaxed))  ;  if (x.load(std::memory_order_relaxed))  ++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); } 
再看下使用acquire-release模型的代碼會(huì)不會(huì)觸發(fā)assert:
#include #include #include std::atomic<bool> x, y;std::atomic<int> z;void write_x_then_y() { x.store(true, std::memory_order_relaxed); y.store(true, std::memory_order_release);} void read_y_then_x() { while (!y.load(std::memory_order_acquire)) ; if (x.load(std::memory_order_relaxed)) ++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);}
使用先后一致次序模型的代碼這里就不過多介紹了,atomic的默認(rèn)次序,肯定沒問題的。所以在我們平時(shí)開發(fā)過程中,普通開發(fā)者不用管那么多,使用默認(rèn)的atomic次序就行,資深程序員可以自由選用,充分利用更加細(xì)分的次序關(guān)系來提升性能,比如寫一個(gè)高性能的無鎖隊(duì)列。一般使用默認(rèn)的atomic足以,我估計(jì)大多數(shù)人寫的代碼,性能瓶頸一般都在業(yè)務(wù)邏輯上,而不是這種內(nèi)存模型上。這里還有個(gè)memory fence的概念,大體作用和上面介紹的類似,感興趣的可以自己了解一下哈。寫到這里,推薦大家看看這段無鎖隊(duì)列的代碼 https://github.com/taskflow/taskflow/blob/master/taskflow/core/tsq.hpp ,有助于理解C++的內(nèi)存模型。


本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時(shí)聯(lián)系本站刪除。
關(guān)閉