當前位置:首頁 > 公眾號精選 > C語言與CPP編程
[導讀]C++11其實主要就四方面內(nèi)容,第一個是可變參數(shù)模板,第二個是右值引用,第三個是智能指針,第四個是內(nèi)存模型(Memory Model)。相對來說,這也是較難理解的幾個特性,分別針對于泛型編程,內(nèi)存優(yōu)化,內(nèi)存管理和并發(fā)編程。

C++11其實主要就四方面內(nèi)容,第一個是可變參數(shù)模板,第二個是右值引用,第三個是智能指針,第四個是內(nèi)存模型(Memory Model)。

相對來說,這也是較難理解的幾個特性,分別針對于泛型編程,內(nèi)存優(yōu)化,內(nèi)存管理和并發(fā)編程。

并發(fā)編程是個非常大的模塊,而在諸多內(nèi)容底下有一個基本的概念,就是并發(fā)內(nèi)存模型(Memory Model)。

那么,什么是內(nèi)存模型?

1
Memory Model

早在之前介紹并發(fā)編程的文章中,我們就知道同步共享數(shù)據(jù)很重要。而同步可分為兩種方式:原子操作和順序約束。

原子操作是數(shù)據(jù)操作的最小單元,天生不可再分;順序約束可以協(xié)調(diào)各個線程之間數(shù)據(jù)訪問的先后順序,避免數(shù)據(jù)競爭。

通常的同步方式會有兩個問題,一是效率不夠,二是死鎖問題。導致效率不夠是因為這些方式都是lock-based的。

當然,若非非常在意效率,完全可以使用這些同步方式,因其簡單方便且不易出錯。

若要追求更高的效率,需要學習lock-free(無鎖)的同步方式。

內(nèi)存模型,簡單地說,是一種介于開發(fā)者和系統(tǒng)之間的并發(fā)約定,可以無鎖地保證程序的執(zhí)行邏輯與預期一致。

這里的系統(tǒng)包括編譯器、處理器和緩存,各部分都想在自己的領域對程序進行優(yōu)化,以提高性能,而這些優(yōu)化會打亂源碼中的執(zhí)行順序。尤其是在多線程上,這些優(yōu)化會對共享數(shù)據(jù)造成巨大影響,導致程序的執(zhí)行結果往往不遂人意。

內(nèi)存模型,就是來解決這些優(yōu)化所帶來的問題。主要包含三個方面:

  • Atomic operations(原子操作)

  • Partial?ordering of operations(局部執(zhí)行順序)

  • Visible effects of operations(操作可見性)

原子操作和局部執(zhí)行順序如前所述,「操作可見性」指的是不同線程之間操作共享變量是可見的。

原子數(shù)據(jù)的同步是由編譯器來保證的,而非原子數(shù)據(jù)需要我們自己來規(guī)劃順序。

2

關系定義

這里有三種關系術語,

  • sequenced-before

  • happens-before

  • synchronizes-with

同一線程語句之間,若A操作在B操作之前執(zhí)行,則表示為A sequenced-before B,A的執(zhí)行結果對B可見。
而在不同線程的語句之間,若A操作在B操作之前就已發(fā)生,則表示為A happens-before B。該關系具有可傳遞性,也就是說,若A happens-before B,B happens-before C,則一定能得出A happens-before C。
若A操作的狀態(tài)改變引發(fā)了B操作的執(zhí)行,則表示為A synchronizes-with B。比如我們學過的事件、條件變量、信號量等等都會因一個條件(狀態(tài))滿足,而執(zhí)行相應的操作,這種狀態(tài)關系就叫做synchronizes-with。
由于synchronizes-with的特性,可以借其實現(xiàn)happens-before關系。
內(nèi)存模型就是提供一個操作的約束語義,借其可以滿足上述關系,實現(xiàn)了順序約束。

3

Atomics(原子操作)

原子操作的知識之前也介紹過,限于篇幅,便不再捉細節(jié)。
先來整體看一下原子操作支持的操作類型,后面再來講應用。
這里挑兩個來介紹一下相關操作,算是回顧。
第一個來講atomic_flag,這是最簡單的原子類型,代表一個布爾標志,可用它實現(xiàn)一個自旋鎖:

1#include?
2#include?
3#include?
4
5class?spin_lock
6{

7????std::atomic_flag?flag?=?ATOMIC_FLAG_INIT;
8public:
9????void?lock()?{?while(flag.test_and_set());?}
10
11????void?unlock()?{?flag.clear();?}
12};
13
14spin_lock?spin;
15int?g_num?=?0;
16void?work()
17
{
18????spin.lock();
19
20????g_num++;
21
22????spin.unlock();
23}
24
25int?main()
26
{
27????std::thread?t1(work);
28????std::thread?t2(work);
29????t1.join();
30????t2.join();
31
32????std::cout?<33
34????return?0;
35}

atomic_flag必須使用ATOMIC_FLAG_INIT初始化,該值就是0,也就是false。
只能通過兩個接口來操作atomic_flag:
  • clear:清除操作,將值設為false。

  • test_and_set:將值設為true并返回之前的值。


第9行的lock()函數(shù)實現(xiàn)了自旋鎖,當?shù)谝粋€線程進來的時候,由于atomic_flag為false,所以會通過test_and_set設置為true并返回false,第一個線程于是可以接著執(zhí)行下面的邏輯。

當?shù)诙€線程進來時,flag為true,因此會一直循環(huán),只有第一個線程中unlock了才會接著執(zhí)行。由此保證了共享變量g_num。
第二個來講atomic,它所支持的原子操作要比atomic_flag多。
一個簡單的同步操作:

1#include?
2#include?
3#include?
4#include?
5#include?
6#include?
7
8std::atomic<bool>?flag{false};
9std::vector<int>?shared_values;
10void?work()
11
{
12????std::cout?<"waiting"?<std::endl;
13????while(!flag.load())
14????{
15????????std::this_thread::sleep_for(std::chrono::milliseconds(5));
16????}
17
18????shared_values[1]?=?2;
19????std::cout?<"end?of?the?work"?<std::endl;
20}
21
22void?set_value()
23
{
24????shared_values?=?{?7,?8,?9?};
25????flag?=?true;
26????std::cout?<"data?prepared"?<std::endl;
27}
28
29int?main()
30
{
31????std::thread?t1(work);
32????std::thread?t2(set_value);
33????t1.join();
34????t2.join();
35
36????std::copy(shared_values.begin(),?shared_values.end(),?std::ostream_iterator<int>(std::cout,?"?"));
37
38????return?0;
39}

這里有兩個線程,它們之間擁有執(zhí)行順序,只有先在set_value函數(shù)中設置好共享值,才能在work函數(shù)中修改。
通過flag的load函數(shù)可以獲取原子值,在值未設置完成時其為false,所以會一直等待數(shù)據(jù)到來。當flag變?yōu)閠rue時,表示數(shù)據(jù)已經(jīng)設置完成,于是會繼續(xù)工作。

4

Memory ordering(內(nèi)存順序)

是什么保證了上述原子操作能夠在多線程環(huán)境下同步執(zhí)行呢?

其實在所有的原子操作函數(shù)中都有一個可選參數(shù)memory_order。比如atomic的load()和store()原型如下:

bool?std::_Atomic_bool::load(std::memory_order?_Order?=?std::memory_order_seq_cst)?const?noexcept
void?std::_Atomic_bool::store(bool?_Value,?std::memory_order?_Order?=?std::memory_order_seq_cst)?noexcept

這里的可選參數(shù)默認為memory_order_seq_cst,所有的memory_order可選值為:

enum?memory_order?{

????memory_order_relaxed,
????memory_order_consume,
????memory_order_acquire,
????memory_order_release,
????memory_order_acq_rel,
????memory_order_seq_cst
};

這就是C++提供的如何實現(xiàn)順序約束的方式,通過指定特定的memory_order,可以實現(xiàn)前面提及的sequence-before、happens-before、synchronizes-with關系。

順序約束是我們和系統(tǒng)之間的一個約定,約定強度由強到弱可以分為三個層次:

  • Sequential consistency(順序一致性): memory_order_seq_cst
  • Acquire-release(獲取與釋放): memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel
  • Relaxed(松散模型): memory_order_relaxed

Sequential consistency保證所有操作在線程之間都有一個全局的順序,Acquire-release保證在不同線程間對于相同的原子變量的寫和讀的操作順序,Relaxed僅保證原子的修改順序。

為何要分層次呢?

其實順序約束和系統(tǒng)優(yōu)化之間是一種零和博弈,約束越強,系統(tǒng)所能夠做的優(yōu)化便越少。

因此每個層次擁有效率差異,層次越低,優(yōu)化越多,效率也越高,不過掌握難度也越大。

所有的Memory order按照操作類型,又可分為三類:

  • Read(讀):memory_order_acquire,memory_order_consume

  • Write(寫):memory_order_release

  • Read-modify-Write(讀-改-寫):memory_order_acq_rel,memory_order_seq_cst

Relaxed未定義同步和順序約束,所以要單獨而論。

例如load()就是Read操作,store()就是Write()操作,compare_exchange_strong就是Read-modify-Write操作。

這意味著你不能將一個Read操作的順序約束,寫到store()上。例如,若將memory_order_acquire寫到store()上,不會產(chǎn)生任何效果。

我們先來從默認的Sequential consistency開始,往往無需設置,便默認是memory_order_seq_cst,可以寫一個簡單的生產(chǎn)者-消費者函數(shù):

1std::string?sc_value;
2std::atomic<bool>?ready{false};
3
4void?consumer()
5
{
6????while(!ready.load())?{}
7
8????std::cout?<std::endl;
9}
10
11void?producer()
12
{
13????sc_value?=?"produce?values";
14????ready?=?true;
15}
16
17int?main()
18
{
19????std::thread?t1(consumer);
20????std::thread?t2(producer);
21????t1.join();
22????t2.join();
23
24????return?0;
25}

此時,執(zhí)行順序具有強保證性,一定是先執(zhí)行了producer再執(zhí)行的consumer。
用標準的關系術語來說就是,第13行的操作和第14行的操作是sequenced-before關系,第14行和第6行的操作是synchronizes-with關系,進而保證了14行的賦值操作一定在第6行的load()操作之前執(zhí)行,也就是保證了happens-before關系。
Acquire-release就開始變得有些復雜,我們先以一個最簡單的例子來看。

1class?spin_lock
2{

3????std::atomic_flag?flag?=?ATOMIC_FLAG_INIT;
4public:
5????spin_lock()?{}
6
7????void?lock()?{?while(flag.test_and_set(std::memory_order_acquire));?}
8
9????void?unlock()?{?flag.clear(std::memory_order_release);?}
10};
11
12spin_lock?spin;
13void?work()
14
{
15????spin.lock();
16????//?do?something
17????spin.unlock();
18}
19
20int?main()
21
{
22????std::thread?t1(work);
23????std::thread?t2(work);
24????t1.join();
25????t2.join();
26
27????return?0;
28}

clear()中使用了release,test_and_set()中使用了acquire,acquire和release操作之間是synchronizes-with的關系。

它的行為和之前使用sequential consistency默認參數(shù)的自旋鎖一樣,不過要更加輕便高效。

test_and_set()操作其實是個Read-modify-Write操作,不過依舊可以使用acquire操作。release禁止了所有在它之前或之后的寫操作亂序,acquire禁止了所有在它之前或之后的讀操作亂序。

在兩個不同的線程之間,共同訪問同一個原子是flag,所添加的順序約束就是為了保證flag的修改順序。

我們再來看一個更清晰的例子:

1std::atomic<bool>?x{false},?y{false};
2std::atomic<int>?z{0};
3void?write()
4
{
5????//?relaxed只保證修改順序
6????x.store(true,?std::memory_order_relaxed);
7
8????//?release保證在它之前的所有寫操作順序一致
9????y.store(true,?std::memory_order_release);
10}
11
12void?read()
13
{
14????//?acquire保證在它之前和之后的讀操作順序一致
15????while(!y.load(std::memory_order_acquire));
16
17????//?relaxed只保證修改順序
18????if(x.load(std::memory_order_relaxed))
19????????++z;
20}
21
22int?main()
23
{
24????std::thread?t1(write);
25????std::thread?t2(read);
26????t1.join();
27????t2.join();
28
29????assert(z.load()?!=?0);
30
31????return?0;
32}

注意這是使用了relaxed、release和acquire三種約束。
relaxed只保證修改順序,所以對于write()函數(shù)來說,一定是先執(zhí)行x后執(zhí)行y操作。不過若是將y也使用relaxed,雖然在write()中是先x后y的順序,而在read()的眼中,可能是先y后x的順序,這是優(yōu)化導致的。
而因為y的讀和寫使用了acquire和release約束,所以可以保證在不同線程間對于相同的原子變量讀和寫的操作順序一致。
同時,Acquire-release操作還擁有傳遞性,是典型的happens-before關系。
還是提供一個例子:

1std::vector<int>?shared_value;
2std::atomic<bool>?produced{false};
3std::atomic<bool>?consumed{false};
4
5void?producer()
6
{
7????shared_value?=?{?7,?8,?9?};
8
9????//?A.?realse happens-before B
10????produced.store(true,?std::memory_order_release);
11}
12
13void?delivery()
14
{
15????//?B.?acquire,A?synchronizes?with?B
16????while(!produced.load(std::memory_order_acquire));
17
18????//?B.?release happens-beforeC
19????consumed.store(true,?std::memory_order_release);
20}
21
22void?consumer()
23
{
24????//?C.?acquire,?B?synchronizes?with?C
25????//?therefore,?A?happens?before?C
26????while(!consumed.load(std::memory_order_acquire));
27
28????shared_value[1]?=?2;
29}
30
31int?main()
32
{
33????std::thread?t1(consumer);
34????std::thread?t2(producer);
35????std::thread?t3(delivery);
36????t1.join();
37????t2.join();
38????t3.join();
39
40????std::copy(shared_value.begin(),?shared_value.end(),?std::ostream_iterator<int>(std::cout,?"?"));
41
42????return?0;
43}

注釋已經(jīng)足夠說明其中所以,便不細述。

5

Fences(柵欄)

看回先前的一個例子:

1std::atomic<bool>?x{false},?y{false};
2std::atomic<int>?z{0};
3void?write()
4
{
5????//?relaxed只保證修改順序
6????x.store(true,?std::memory_order_relaxed);
7????y.store(true,?std::memory_order_relaxed);
8}
9
10void?read()
11
{
12????//?relaxed只保證修改順序
13????while(!y.load(std::memory_order_relaxed));
14????if(x.load(std::memory_order_relaxed))
15????????++z;
16}


relaxed是最弱的內(nèi)存模型,此處全使用relaxed,順序將不再有保證。

也許在read()中看到的write()操作是先y后x,那么此時read()里面的if操作便無法滿足,也就是說,++z不會被執(zhí)行。

解決方法是結合fences來使用,只需添加兩行代碼:

1std::atomic<bool>?x{false},?y{false};
2std::atomic<int>?z{0};
3void?write()
4
{
5????//?relaxed只保證修改順序
6????x.store(true,?std::memory_order_relaxed);
7
8????std::atomic_thread_fence(std::memory_order_release);
9
10????y.store(true,?std::memory_order_relaxed);
11}
12
13void?read()
14
{
15????//?relaxed只保證修改順序
16????while(!y.load(std::memory_order_relaxed));
17
18????std::atomic_thread_fence(std::memory_order_acquire);
19
20????if(x.load(std::memory_order_relaxed))
21????????++z;
22}

fences位于relaxed操作之間,它像一個柵欄一樣,可以保證前后的操作不會亂序。
具體細節(jié),接著來看。
C++提供了兩個類型的fences,
  • std::atomic_thread_fence:同步線程之間的內(nèi)存訪問。

  • std::atomic_signal_fence:同步同一線程上的signal handler和code running。

我們主要學習第一個線程的fence,它會阻止特定的操作穿過柵欄,約束執(zhí)行順序。

有三種類型的fences,

  • Full fence:阻止兩個任意操作亂序。memory_order_seq_cst或memory_order_acq_rel。

  • Acquire fence:阻止讀操作亂序,memory_order_acquire。

  • Release fence:阻止寫操作亂序,memory_order_release。

用圖來表示為:

圖中間灰色的一杠就表示fence,紅色表示禁止亂序,可以看到,除了Store-Load,其它操作都可以保障執(zhí)行順序。
同樣也有效率差異,可以針對具體的操作來選擇合適的fence。

6

總結

本篇內(nèi)容挺復雜的,其實就包含三個方面:Atomic operations(原子操作)、Partial?ordering of operations(局部執(zhí)行順序)和Visible effects of operations(操作可見性)。

面對一個復雜的概念,往往需要變換尺度來進行理解,若一開始便陷入諸多細節(jié)中去,難免迷失其中,看不到整體的結構。
所以這里其實也就是以我自己的理解來寫,細節(jié)涉及不多,但整體結構已算完整,想了解更多具體細節(jié)可以參考C++ Concurrency in Action。

免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

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

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉