【硬核】C++11併發:記憶體模型和原子型別

阿里妹導讀
本文從C++11併發程式設計中的關鍵概念——記憶體模型與原子型別入手,結合詳盡的程式碼示例,抽絲剝繭地介紹瞭如何實現無鎖化併發的效能最佳化。
引言
在遊戲後臺領域C++一直是主流開發語言。就算是在伺服器效能已經非常強大的今天,我依然覺得遊戲行業對於效能的突破的需求還遠沒有結束。大家手機遊戲動輒要120hz,這意味著遊戲的戰鬥服的幀數必須超過120hz。以前我一臺伺服器裝下幾十人線上戰鬥刷副本已經很不錯了,但是像萬龍覺醒型別的SLG動輒就是幾百好人同屏戰鬥,上萬人在同一臺伺服器戰鬥,基於此每一臺機器的效能都必須充分壓榨。無鎖化併發的效能最佳化需要後臺開發者熟悉C++的記憶體模型和原子型別的使用。
一、記憶體模型基礎
記憶體模型有兩個方面:一個是基本結構,這與記憶體中儲存資料的佈局有關;另外一個是併發效能方面。資料結構對於併發效能很重要,特別在於 low-level 的原子操作。下面將從物件和記憶體地址開始介紹。
1.1 物件和記憶體地址
與Java,Ruby等面嚮物件語言普遍理解的 “everythings is object” 不同,C++的物件是 “a  region of storage”。C++的物件是一個緊密聚集在一塊的記憶體區域。有四個重要的特點:
(1)每一個變數都是一個物件,包括成員變數;
(2)每一個物件至少佔用一個記憶體地址;
(3)基本變數無論多大都只有一個記憶體地址;
(4)相鄰bit域下的記憶體地址是一致的。
以上這些特點怎麼理解呢,和我們的系統的記憶體分配機制有關係哈,舉一個栗子就能明白。
classzoo{public:int m_number;  Pig m_onePig;  PigHome* p_pigHome;}
上面定義了一個C++類,當我們在記憶體中建立一個zoo物件時其實是劃分了一個連續的記憶體區域,並且賦予了一個記憶體地址指向這個記憶體區域,這就是特點2,至少佔用一個記憶體地址。zoo裡面定義了成員變數 m_number,m_onePig和p_pigHome。因為m_number和p_pigHome是基本變數(指標算特殊基本變數)本身只有一個記憶體地址的,命中特點3。在zoo開闢的這塊記憶體地址是連續的,m_number,m_onePig和p_pigHome的記憶體地址,都屬於zoo的記憶體地址內,這是特點4。
1.2 物件,記憶體地址與併發
C++ 多執行緒應用最重要的點:都取決於記憶體地址。如果有執行緒更新多個執行緒共享的記憶體地址的內容就存在競態條件(the race condition)。為了避免競態條件,就需要使多個執行緒按順序訪問資源。一種方式是使用互斥鎖(mutex);另外一種就是使用具有原子性的(atomic)同步屬性去操作同一個記憶體地址或者其他記憶體地址,使得執行緒間的訪問有序化。
對於同一個記憶體的訪問如果沒有強制的順序的話,資料競爭導致的情況是未定義的。
1.3 修改順序
從物件的初始化開始,C++物件都有定義一個修改順序,這個順序由所有執行緒的寫組成。大多數情況下,每次程式執行的順序都是不同的,但是全部執行緒都要同意按這順序進行修改。如果修改的物件不是原子性的,就需要使用必要的同步工具(互斥鎖等)保證執行緒對每一個變數的修改順序達成一致。如果使用原子操作(atomic operations),則編譯器負責保證同步的到位。
這使得某些型別的推測執行是不被允許的。執行緒執行修改的過程中,看到一個特定的條目後,這個執行緒後續的讀必須返回最新值,並且後續的寫必須發生在修改之後。同樣,在同一個執行緒中,寫一個物件的值之後再讀這個物件的值,那麼必須是最新寫的值。
C++執行順序問題執行順序問題比較複雜,這裡稍微展開方便對後面內容的理解。首先,在程式碼沒有特別標誌的情況下,主流的C++編譯器為了達到更好的執行效率,往往會對我們所寫的程式碼進行編譯重排。這就導致了實際執行的程式碼和我們所寫的程式碼的順序有所不同。此外,程式執行過程實際是一條條CPU指令,為了執行過程中CPU流水線效率的最大化,在不影響結果的情況下允許執行指令的重排。以上重排都基於保證單執行緒執行結果的正確性的。另外,現在主流的硬體都是多核架構,不同執行緒可能執行在不同的CPU核心上。這存在不同執行緒對同一個寫資料可能發生在不同核心的快取中,而快取有效性和資料寫回實際記憶體的時機是需要同步來保證的。C++將這種保證也交給到了開發者,開發者可以靈活使用,最佳化執行效率。
二、C++ 標準庫提供的原子型別
原子操作不可分割,只有完成和未完成,無中間態。如果讀寫操作都是原子的,那麼讀到的資料要麼是初始值,要麼就是修改後的值。對於一個非原子資料的同時讀寫,就會存在競態,導致讀取到的值是未定義的。我們很快就會想到使用互斥鎖來保證這樣的讀寫。此外,C++對常用的資料型別都提供了原子型別,透過編譯器在編譯最佳化中保障讀寫過程的同步(往往比互斥鎖的效率要高) 。
2.1 標準原子型別
上面這個表格列出的是C++基本資料型別的原子型別。這些原子型別的方法和使用規則可以參考這個連結:C++11 原子型別與原子操作[1],篇幅有限不一一說明。下面列出一些個人認為比較關鍵的點。
2.2 原子型別比較關鍵的點
(1) 標準原子型別是不能複製和賦值,他們沒有複製建構函式和複製賦值操作;
(2)C++ 原子類很多是無鎖實現的,但是這也與編譯器和其執行的平臺有關,所以可以透過 is_lock_free() 來判斷是否是無鎖的;
(3)根據當前值判定是否儲存新值(CAS,Compare and exchange):原子型別的為CAS操作提供了兩個方法分別是:
  • compare_exchange_weak()
  • compare_exchange_strong()
在一些不支援 compare-and-exchange 單指令的機器上,如果處理器不能保證操作以原子方式完成,compare_exchange_week()即使期望值與當前值是相等的也會儲存新值失敗。(PS:作業系統的執行緒比處理器多時,執行操作的執行緒在必要的指令序列中間被切換掉,另一個執行緒被作業系統安排在它的位置上。這種情況被稱為 spurious failure)。compare_exchange_strong()則保證不會有spurious failure。
為了避免可能存在的 spurious failure,compare_exchange_weak() 可以採取自旋的方式使用:
bool expected = false;extern atomic<bool> b;while(!b.compare_exchange_weak(expected, true) && !expected);// 當期望值與實際值不等,會修改期望值為實際值
簡單值建議使用compare_exchange_weak(),計算複雜和儲存耗時的值可以使用compare_exchange_strong()。如果確定處理器不會出現spurious failure(例如 X86)直接使用compare_exchange_weak()。
(4)雖然有std::atomic<>,但是不要使用自定義的原子型別。原因有很多,例如原子型別的CAS操作是基於memcmp()和memcpy()的,而atomic<float>等浮點型別有有精度誤差的,不能保證CAS的正確性;編譯器通常都會使用內部鎖來處理這類自定義的原子型別,如果自定義型別存在預設的複製賦值就會帶來很大的問題。總而言之,自定義型別很難保證型別本身完全符合原子型別的標準,不要去使用自定義的原子型別。
(5)C++指標是有原子型別的:std::atomic<T*>。
三、同步操作和強制排序
假設有兩個執行緒,其中一個準備去填充一塊資料,為了確保這塊資料沒有競態問題,它為這個資料設定了一個標記來指示這塊資料是否是準備好的,另外一個執行緒直到這個標記被設定了才能讀這塊資料。下面的程式碼是一個簡單的例子供參考。
std::vector<int> data;std::atomic<bool> data_ready(false);voidreader_thread(){while(!data_ready.load())    {std::this_thread::yield();    }std::cout << "data is" << data[0]  << std::endl;}voidwrite_thread(){    data.push_back(1);    data_ready = true; //  std::atomic<bool> 過載了 = }
上面這段程式碼這段程式碼非常符合直覺,程式會輸出"1"。這段程式碼中原子變數data_ready 提供了必要的記憶體序列模型 happens-before 和 synchronizes_with。
  • 寫 data 發生於寫 data_ready 之前;
  • 讀 data_ready 發生於讀 data 之前;
happens-before是符合傳遞律的,所以寫 data 發生於讀 data 之前,這也就意味著,data_ready 設定為 true時,data 的寫已經同步給了data 的讀。上面的程式碼是存在一個強制設定的順序的。
3.1 synchronized-with 關係
下面就是介紹兩種同步關係了,首先講synchronized-with 關係。在我把它翻譯成同步對應關係。
只有在原子型別的操作之間才能獲得synchronized -with關係。如果資料結構包含原子型別,並且對該資料結構的操作在內部執行適當的原子操作,則資料結構上的操作(例如鎖定互斥鎖)可能提供這種關係,但基本上它只來自於對原子型別的操作。
如果執行緒A儲存了一個值,而執行緒B讀取了這個值,那麼執行緒A中的儲存和執行緒B中的讀之間就存在synchronized -with關係(這個值是原子型別的哦,不是的話這個過程會導致未知的結果)。
為了能更通俗的理解,就是一個執行緒1對原子變數x進行寫的操作,就需要保障其他讀x值的執行緒能正確讀到x被修改後的值。
3.2 happens-before 關係
happens-before 關係是程式中操作順序的基本結構塊,它指定了哪些操作可以看到其他操作的效果。我把他翻譯成前後關係。如果一個執行緒上的操作A發生在另一個執行緒上的操作B之前,那麼A和B就有前後關係了。
happens-before是符合傳遞律的。A happens-before B , B happens-before C, then A happens-before C。這個傳遞規律也可以被總結為 ordered-before 關係(排序前後關係)。
3.3 原子操作的記憶體序標記
下面間介紹用於原子操作的記憶體序標記和他們的同步關係(synchronized-with)。C++提供了6種記憶體排序如下表所示:參考傳送門C++之Memory order[2]

3.3.1 順序一致序列(sequentially consistent ordering)

C++ 原子型別的預設就是順序一致性排序的,使用的是:
std::memory_order_seq_cst 來標識。從同步的角度來說,對於同一個原子變數的讀寫,與宣告的順序是一致的。這種情況符合人的直覺,使用一個簡單例子來說明這個一致性規則。
#include<atomic>#include<thread>#include<iostream>#include<assert.h>std::atomic<bool> x,y;voidwrite_x(){    x.store(true,std::memory_order_seq_cst);// x 設定為true}voidwrite_y(){    y.store(true,std::memory_order_seq_cst);// y 設定為true}voidread_x_then_y(){while(x.load(std::memory_order_seq_cst) == false);if(y.load(std::memory_order_seq_cst) == true)    {std::cout << "ok1" << std::endl;// x變成true時候y也是true則輸出ok1    }}voidread_y_then_x(){while(y.load(std::memory_order_seq_cst) == false);if(x.load(std::memory_order_seq_cst) == true)    {std::cout << "ok2" << std::endl;// y變成true時候x也是true則輸出ok2    }}intmain(){    x = false;    y = false;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();}
上面的這段程式碼的結果是總都會打出ok,可能列印兩次也可能是一次。情況1:x變成true時,如果y還是false則 read_x_then_y() 提起結束, "ok1" 不會輸出,此時read_y_then_x()還在等待,直到y變成true,此時x已經變成true,"ok2" 列印;情況2:y變成true,x還是false,和情況1類似,列印 “ok1”;情況三,x和y都是true,列印 "ok1" 和 “ok2”。
順序一致性規則是最直接和符合直覺的排序規則,但是它是最費記憶體的,因為需要全部執行緒進行全域性的同步。在多處理器系統下還需要額外花銷,多個處理器間需要的同步通訊的時間花銷。

3.3.2 鬆散序列(relaxed ordering)

鬆散排序的原子操作,不構成 synchronized-with 關係。在同一個執行緒中執行對同一個原子變數仍然保持著happen-before的關係(保序),但是跨執行緒的原子操作不保序。相對於順序一致排序,使用鬆散排序可能使不同的執行緒看到的變數修改順序不一致。下面是鬆散排序的一個簡單例子。
#include<atomic>#include<thread>#include<assert.h>std::atomic<bool> x,y;std::atomic<int> z;voidwrite_x_then_y(){    x.store(true,std::memory_order_relaxed);// 保證 x 的原子寫    y.store(true,std::memory_order_relaxed);// 保證 y 的原子寫}voidread_y_then_x(){while(!y.load(std::memory_order_relaxed));if(x.load(std::memory_order_relaxed))      ++z;// 如果 y為 true x 已經為ture 則 z++}intmain(){    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);}
上述過程的斷言可能會發生!std::memory_order_relaxed 只保證了同一執行緒內同一原子變數的 happen-before 關係(注意這是在同一原子變數)。線上程write_x_then_y() 中寫a和寫b是可以自由重排的,因為他們沒有強制的happens-before 關係。此外,即使線上程write_x_then_y() 中 ,x 先設定為 true,y 再設定為 true,由於不保證同步關係,即x變成true和y變成true這個事情並不一定保證按照這個修改順序通知給執行緒 read_y_then_x()。下面是一個稍微複雜的例子。
#include<thread>#include<atomic>#include<iostream>usingnamespacestd;atomic_intx(0), y(0), z(0);atomic_boolgo(false);constunsignedintloop_count(10);structread_value{int x, y, z;};read_value v1[loop_count];read_value v2[loop_count];read_value v3[loop_count];read_value v4[loop_count];read_value v5[loop_count];voidincrement(atomic_int* var_to_inc, read_value* read_value_ptr){while (!go)std::this_thread::yield();for(unsigned i = 0; i < loop_count; i++)    {        read_value_ptr[i].x = x.load(std::memory_order_relaxed);        read_value_ptr[i].y = y.load(std::memory_order_relaxed);        read_value_ptr[i].z = z.load(std::memory_order_relaxed);        var_to_inc->store(i + 1, std::memory_order_relaxed);std::this_thread::yield();    }}voidread_vals(read_value* read_value_ptr){while (!go)std::this_thread::yield();for(unsigned i = 0; i < loop_count; i++)    {        read_value_ptr[i].x = x.load(std::memory_order_relaxed);        read_value_ptr[i].y = y.load(std::memory_order_relaxed);        read_value_ptr[i].z = z.load(std::memory_order_relaxed);std::this_thread::yield();    }}voidprint(read_value* read_value_ptr){for(unsigned i = 0; i < loop_count; i++)    {if(i)        {cout << ",";        }cout << "(" << read_value_ptr[i].x << "," << read_value_ptr[i].y << "," << read_value_ptr[i].z << ")";    }cout << endl;}intmain(){std::thread t1(increment, &x, v1);std::thread t2(increment, &y, v2);std::thread t3(increment, &z, v3);std::thread t4(read_vals, v4);std::thread t5(read_vals, v5);    go = true;    t5.join();    t4.join();    t3.join();    t2.join();    t1.join();    print(v1);    print(v2);    print(v3);    print(v4);    print(v5);return0;}
這段程式碼看似複雜其實就是對原子型別的x,y,z的10次由1到10的賦值運算。執行緒 t1 負責 x 的由1到10的10次賦值,v1 負責記錄10次賦值中 t1 讀取到的資料三元組(x, y, z)的數值變化。執行緒t2 負責 y 的由1到10的10次賦值,三元組(x, y, z)數值變化記錄在v2。執行緒t3 負責 z 的由1到10的10次賦值,三元組(x, y, z)數值變化記錄在v3。執行緒t4和執行緒t5都是觀察記錄三元組(x, y, z)的變化,分別記錄在v4和v5。
這段程式碼的一個執行結果:
(0,0,0),(1,3,1),(2,3,2),(3,7,3),(4,8,4),(5,10,5),(6,10,6),(7,10,7),(8,10,8),(9,10,9)(0,0,0),(0,1,0),(1,2,1),(1,3,1),(2,4,2),(2,5,2),(2,6,2),(4,7,4),(4,8,4),(4,9,4)(1,2,0),(2,3,1),(2,4,2),(4,7,3),(4,8,4),(6,10,5),(7,10,6),(8,10,7),(8,10,8),(10,10,9)(0,0,0),(0,0,0),(0,1,0),(0,1,0),(0,2,1),(1,2,1),(1,3,1),(1,3,1),(1,3,1),(1,3,1)(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10)
上面的結果顯示可以看出:
1.執行緒2對y的累加過程中,x和z有3輪是沒有變化的;
2.執行緒4對整個資料的監控會發現,資料有好幾輪是沒有同步的;
3.整體資料非常鬆散;
如果取消鬆散排序採用預設一致性排序的一種結果如下。相比之下,資料更加緊湊。如果有興趣可以改一下程式碼,測試兩種方式的執行效率。
(0,0,0),(1,3,1),(2,3,2),(3,4,3),(4,5,4),(5,6,4),(6,10,6),(7,10,7),(8,10,8),(9,10,8)(0,0,0),(0,1,0),(1,2,1),(2,3,2),(3,4,3),(3,5,3),(5,6,4),(5,7,4),(5,8,6),(6,9,6)(1,2,0),(2,3,1),(3,3,2),(3,4,3),(5,6,4),(6,8,5),(6,10,6),(8,10,7),(9,10,8),(10,10,9)(2,3,3),(3,4,3),(3,4,3),(3,4,3),(3,5,3),(3,5,3),(3,5,3),(3,5,3),(3,5,4),(5,6,4)(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10)

3.3.3 獲取-釋放序列(acquire-release ordering)

獲取-釋放序列存在一定的同步關係,但是不是全域性的。此記憶體模型下:
原子讀(std::memory_order_acquire)是 acquire;
原子寫(std::memory_order_release)是 release;
原子RMW(read-motify-write)操作(例如 fetch_add() 或者 exchange)
可以是acquire,release ,也可以是二者的合併(std::memory_order_acq_rel)。
執行緒間的讀寫是一種成對的關係,他們之間是有同步關係的。不同執行緒還是會看得不同的執行順序,但是這個執行順序是被限制的。怎麼解釋上面說所的執行順序的限制,看下面的例子。
#include<atomic>#include<thread>#include<iostream>#include<assert.h>std::atomic<bool> x,y;std::atomic<int> z;voidwrite_x(){    x.store(true,std::memory_order_seq_release);// x 設定為true}voidwrite_y(){    y.store(true,std::memory_order_seq_release);// y 設定為true}voidread_x_then_y(){while(x.load(std::memory_order_seq_acquire) == false);if(y.load(std::memory_order_seq_acquire) == true)    {std::cout << "ok1" << std::endl;// x變成true時候y也是true則輸出ok1    }}voidread_y_then_x(){while(y.load(std::memory_order_seq_acquire) == false);if(x.load(std::memory_order_seq_acquire) == true)    {std::cout << "ok2" << std::endl;// y變成true時候x也是true則輸出ok2    }}intmain(){    x = false;    y = false;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();}
上面這個例子呢,"ok" 可能不輸出。對於執行緒c來說,讀 x 為 true 時,y 可能是 false。與此同時執行緒d中的 y 為 true 時,x 可能為 false。再總體描述一種情況:執行緒 a 設定 x 為true,同步給執行緒 c 但未同步給執行緒 d,同時執行緒 b 設定 y 為 true,同步給了執行緒 d,未同步給執行緒c。此時執行緒 c 認為 x 為 ture,y 為 false,執行緒 d 認為 y 為 true,x 為 false。由於獲取-釋放序列只保證了部分的先後關係(執行緒a保證寫 x happen-before 執行緒 c 讀 x,不保證 y 的;執行緒 b和d情況上),因此這是一種部分的同步。
由於獲取-釋放序列的部分同步特點,經用在一些有先後順序的場景下使用,一般比順序一致性序列要高效。下面是一個非常經典的例子:
#include<atomic>#include<thread>#include<assert.h>std::atomic<bool> x,y;std::atomic<int> z;voidwrite_x_then_y(){    x.store(true,std::memory_order_relaxed);// x是鬆散式原子寫    y.store(true,std::memory_order_release);// y是釋放式原子寫}voidread_y_then_x(){while(!y.load(std::memory_order_acquire));// y獲取式原子讀if(x.load(std::memory_order_relaxed))// x 鬆散式原子讀        ++z;// 如果y變成ture時 x已經變成 true 則z != 0}intmain(){    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);}
以上程式碼,斷言永遠不會發生。寫 y 保證了寫 x 發生於寫 y 之前,寫 y 和讀 y 是同步的,因此在讀 y 之前 x 已經被修改(符合傳遞律)。如上面說展示的,只有acquire讀和release寫才會形成這種同步。

3.3.4 資料依賴(data dependency )

相比於同步關係,std::memory_order_consume 提供了是相對弱一些的記憶體同步。這種弱同步被稱為是資料依賴(data dependency)。C++中的資料依賴分兩種:
(1)carrary-a-dapendency-to:如果操作A的結果在操作B中被用作運算元,則: A攜帶一個依賴項到B。
(2)dependency-ordered-before:如果讀操作B的結果在同一個執行緒中的進一步操作C中使用,那麼A的寫操作:
(std::memory_order_release, std::memory_order_acq_rel,或std::memory_order_seq_cst)
的依賴序列在讀操作B (std::memory_order_consume)之前。
對於 std::memory_order_consume 標記的一種重要的使用場景就是原子操作一個指標,這個指標指向一個記憶體區域。
#include<atomic>#include<cassert>#include<string>#include<thread>structX {int i_;std::string s_;};std::atomic<int> a;std::atomic<X*> p;voidcreate_x(){  X* x = new X;  x->i_ = 42;  x->s_ = "hello";  a.store(99, std::memory_order_relaxed);  p.store(x, std::memory_order_release);// x 和 p 構成 }voiduse_x(){  X* x;while (!(x = p.load(std::memory_order_consume)));//  assert(x->i_ == 42);  assert(x->s_ == "hello");  assert(a.load(std::memory_order_relaxed) == 99);}intmain(){std::thread t1(create_x);std::thread t2(use_x);  t1.join();  t2.join();return0;}
以上程式碼
assert(a.load(std::memory_order_relaxed)==99) 是可能觸發斷言的。執行緒 t1 對 p 原子寫,執行緒  t2 對 p 原子讀(標記為consume),則 t2 原子讀 p 依賴於 t1 原子寫。在t2中,x 指標 = p指標,則 p 攜帶了一個依賴到了 x 指標,則 t1 對於 p1 的原子寫發生於 x 指標賦值p指標之前。以上的一個推到關係保證了前面兩個assert均不會失敗。而 a 的值並沒這層保證,所以可能會觸發第三個斷言。可以這麼說,只約束對和 p 有關的資料,無關的不約束。

3.3.5 柵欄(fences)

柵欄是一種全域性的操作會影響執行緒內的其他原子操作的順序。柵欄也常被稱為記憶體屏障(memory barriers),因為這種操作會在程式碼里加入一行使得某些操作不能跨越。柵欄限制了編譯器或者硬體對於不相關變數的重排自由,並且引入了之前不存在happens-before 和 synchronizes-with關係。C++11 原子庫定義了可移植的函式 std::atomic_thread_fence() ,該函式接收一個引數用於指定柵欄的型別。
#include<atomic>#include<thread>#include<iostream>#include<assert.h>std::atomic<bool> x,y;voidwrite_x_then_y(){    x.store(true,std::memory_order_relaxed);// x 設定為true 在 fence之前std::atomic_thread_fence(std::memory_order_release);    y.store(true,std::memory_order_relaxed);// y 設定為true 在 fence之後}voidread_y_then_x(){while(y.load(std::memory_order_relaxed) == false);// 自旋 直到 y 被設定為truestd::atomic_thread_fence(std::memory_order_acquire);if(x.load(std::memory_order_relaxed) == true)// x 已經在 y 之前被設定為 true    {std::cout << "ok" << std::endl;// y變成true時候x也是true則輸出ok    }}intmain(){    x = false;    y = false;std::thread a(write_x_then_y);std::thread b(read_y_then_x);    a.join();    b.join();}
上面這個例子 "ok"一定會輸出。std::atomic_thread_fence(std::memory_order_release) 和 std::atomic_thread_fence(std::memory_order_acquire)保證了store x happen before load x。當然 y.store() releasey.load() acquire 也是一樣的,這裡只是用來舉例子。
使用柵欄可以使非原子操作有序化對上面的例子進行一個簡單修改,實現的效果是一致的。雖然對x的操作是非原子的,但是柵欄保證了它在 y 寫之前被寫 y 讀之前被讀。當然這裡對 y 的操作必須是原子性的,如果寫 y 的同時讀 y 這個過程的結果是未定義。
#include<atomic>#include<thread>#include<iostream>#include<assert.h>bool x;std::atomic<bool> y;voidwrite_x_then_y(){    x = true;// x 設定為truestd::atomic_thread_fence(std::memory_order_release);    y.store(true,std::memory_order_relaxed);// y 設定為true}voidread_y_then_x(){while(y.load(std::memory_order_relaxed) == false);std::atomic_thread_fence(std::memory_order_acquire);if(x)    {std::cout << "ok" << std::endl;// y變成true時候x也是true則輸出ok    }}intmain(){    x = false;    y = false;std::thread a(write_x_then_y);std::thread b(read_y_then_x);    a.join();    b.join();}
在 happens-before關係到來之前 sequenced-before關係已經確定,這對於使用原子操作確定非原子操作序列是很重要的。如果同一執行緒中非原子操作在序列在原子操作之前,本執行緒原子操作 happens-before 另外一個執行緒原子操作,則該執行緒的非原子操作happens-before 另外一個執行緒的原子操作。使用C++11標準庫提供的更高等級的同步工具,例如互斥鎖和條件變數也是可以實現的。
四、寫在後面
本文主要參考《C++ Concurrency In Action》,並對其內容做了提煉總結。如果大家對C++併發有興趣,非常推薦大家去拜讀原版《C++ Concurrency In Action》,裡面有非常多的無鎖程式設計模型。
後面預告一個新的面向物件的開發模型,和我們的阿里雲伺服器同名,簡稱就叫ECS(Entity Component System),它被多用於遊戲引擎當中,後面我將這個引入到了後臺遊戲業務開發中,可用於戰鬥服和大地圖等,請期待後面的講解。
參考連結:
[1]https://blog.csdn.net/K346K346/article/details/85345477
[2]https://blog.csdn.net/ji2581072/article/details/139616941
ALB實現跨地域負載均衡
當客戶業務遍及多地,並且在阿里雲多個地域均部署了服務時,使用負載均衡結合雲企業網及轉發路由器可實現應用跨地域級別負載均衡。    
點選閱讀原文檢視詳情。

相關文章