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

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


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