<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊內(nèi)存模型與內(nèi)存序

          共 22322字,需瀏覽 45分鐘

           ·

          2022-07-05 13:23

          最近群里聊到了Memory Order相關(guān)知識(shí),恰好自己對(duì)這塊的理解是模糊的、不成體系的,所以借助本文,重新整理下相關(guān)知識(shí)。

          寫(xiě)在前面

          在真正了解Memory Order的作用之前,曾經(jīng)簡(jiǎn)單地將Memory Order等同于mutex和atomic來(lái)進(jìn)行線程間數(shù)據(jù)同步,或者用來(lái)限制線程間的執(zhí)行順序,其實(shí)這是一個(gè)錯(cuò)誤的理解。直到后來(lái)仔細(xì)研究了Memory Order之后,才發(fā)現(xiàn)無(wú)論是功能還是原理,Memory Order與他們都不是同一件事。實(shí)際上,Memory Order是用來(lái)用來(lái)約束同一個(gè)線程內(nèi)的內(nèi)存訪問(wèn)排序方式的,雖然同一個(gè)線程內(nèi)的代碼順序重排不會(huì)影響本線程的執(zhí)行結(jié)果(如果結(jié)果都不一致,那么重排就沒(méi)有意義了),但是在多線程環(huán)境下,重排造成的數(shù)據(jù)訪問(wèn)順序變化會(huì)影響其它線程的訪問(wèn)結(jié)果。

          正是基于以上原因,引入了內(nèi)存模型。C++的內(nèi)存模型解決的問(wèn)題是如何合理地限制單一線程中的代碼執(zhí)行順序,使得在不使用鎖的情況下,既能最大化利用CPU的計(jì)算能力,又能保證多線程環(huán)境下不會(huì)出現(xiàn)邏輯錯(cuò)誤。

          指令亂序

          現(xiàn)在的CPU都采用的是多核、多線程技術(shù)用以提升計(jì)算能力;采用亂序執(zhí)行、流水線、分支預(yù)測(cè)以及多級(jí)緩存等方法來(lái)提升程序性能。多核技術(shù)在提升程序性能的同時(shí),也帶來(lái)了執(zhí)行序列亂序和內(nèi)存序列訪問(wèn)的亂序問(wèn)題。與此同時(shí),編譯器也會(huì)基于自己的規(guī)則對(duì)代碼進(jìn)行優(yōu)化,這些優(yōu)化動(dòng)作也會(huì)導(dǎo)致一些代碼的順序被重排。

          首先,我們看一段代碼,如下:

          int A = 0;
          int B = 0;

          void fun() {
              A = B + 1// L5
              B = 1// L6
          }

          int main() {
              fun();
              return 0;
          }

          如果使用 g++ test.cc,則生成的匯編指令如下:

          movl    B(%rip), %eax
          addl    $1, %eax
          movl    %eax, A(%rip)
          movl    $1, B(%rip)

          通過(guò)上述指令,可以看到,先把B放到eax,然后eax+1放到A,最后才執(zhí)行B = 1。

          而如果我們使用g++ -O2 test.cc,則生成的匯編指令如下:

          movl    B(%rip), %eax
          movl    $1, B(%rip)
          addl    $1, %eax
          movl    %eax, A(%rip)

          可以看到,先把B放到eax,然后執(zhí)行B = 1,再執(zhí)行eax + 1,最后將eax賦值給A。從上述指令可以看出執(zhí)行B賦值(語(yǔ)句L6)語(yǔ)句先于A賦值語(yǔ)句(語(yǔ)句L5)執(zhí)行。

          我們將上述這種不按照代碼順序執(zhí)行的指令方式稱(chēng)之為指令亂序

          對(duì)于指令亂序,這塊需要注意的是:編譯器只需要保證在單線程環(huán)境下,執(zhí)行的結(jié)果最終一致就可以了,所以,指令亂序在單線程環(huán)境下完全是允許的。對(duì)于編譯器來(lái)說(shuō),它只知道:在當(dāng)前線程中,數(shù)據(jù)的讀寫(xiě)以及數(shù)據(jù)之間的依賴(lài)關(guān)系。但是,編譯器并不知道哪些數(shù)據(jù)是在線程間共享,而且是有可能會(huì)被修改的。而這些是需要開(kāi)發(fā)人員去保證的。

          那么,指令亂序是否允許開(kāi)發(fā)人員控制,而不是任由編譯器隨意優(yōu)化?

          可以使用編譯選項(xiàng)停止此類(lèi)優(yōu)化,或者使用預(yù)編譯指令將不希望被重排的代碼分隔開(kāi),比如在gcc下可用asm volatile,如下:

          void fun() {
              A = B + 1;
              asm volatile("" ::: "memory");
              B = 0;
          }

          類(lèi)似的,處理器也會(huì)提供指令給開(kāi)發(fā)人員使用,以避免亂序控制,例如,x86,x86-64上的指令如下:

          lfence (asm), void _mm_lfence(void)
          sfence (asm), void _mm_sfence(void)
          mfence (asm), void _mm_mfence(void)

          為什么需要內(nèi)存模型

          多線程技術(shù)是為了最大限度地壓榨cpu,提升計(jì)算能力。在單核時(shí)代,多線程的概念是在宏觀上并行,微觀上串行,多線程可以訪問(wèn)相同的CPU緩存和同一組寄存器。但是在多核時(shí)代,多個(gè)線程可能執(zhí)行在不同的核上,每個(gè)CPU都有自己的緩存和寄存器,在一個(gè)CPU上執(zhí)行的線程無(wú)法訪問(wèn)另一個(gè)CPU的緩存和寄存器。CPU會(huì)根據(jù)一定的規(guī)則對(duì)機(jī)器指令的內(nèi)存交互進(jìn)行重新排序,特別是允許每個(gè)處理器延遲存儲(chǔ)并且從不同位置裝載數(shù)據(jù)。與此同時(shí),編譯器也會(huì)基于自己的規(guī)則對(duì)代碼進(jìn)行優(yōu)化,這些優(yōu)化動(dòng)作也會(huì)導(dǎo)致一些代碼的順序被重排。這種指令的重排,雖然不影響單線程的執(zhí)行結(jié)果,但是會(huì)加劇多線程訪問(wèn)共享數(shù)據(jù)時(shí)的數(shù)據(jù)競(jìng)爭(zhēng)(Data Race)問(wèn)題。

          以上節(jié)例子中的A、B兩個(gè)變量為例,在編譯器將其亂序后,雖然對(duì)于當(dāng)前線程是沒(méi)問(wèn)題的。但是在多線程環(huán)境下,如果其它線程依賴(lài)了A 和 B,會(huì)加劇多線程訪問(wèn)共享數(shù)據(jù)的競(jìng)爭(zhēng)問(wèn)題,同時(shí)可能會(huì)得到意想不到的結(jié)果。

          正是因?yàn)橹噶顏y序以及多線程環(huán)境數(shù)據(jù)競(jìng)爭(zhēng)的不確定性,我們?cè)陂_(kāi)發(fā)的時(shí)候,經(jīng)常會(huì)使用信號(hào)量或者鎖來(lái)實(shí)現(xiàn)同步需求,進(jìn)而解決數(shù)據(jù)競(jìng)爭(zhēng)導(dǎo)致的不確定性問(wèn)題。但是,加鎖或者信號(hào)量是相對(duì)接近操作系統(tǒng)的底層原語(yǔ),每一次加鎖或者解鎖都有可能導(dǎo)致用戶(hù)態(tài)和內(nèi)核態(tài)的互相切換,這就導(dǎo)致了數(shù)據(jù)訪問(wèn)開(kāi)銷(xiāo),如果鎖使用不當(dāng),可能會(huì)造成嚴(yán)重的性能問(wèn)題,所以就需要一種語(yǔ)言層面的機(jī)制,既沒(méi)有鎖那樣的大開(kāi)銷(xiāo),又可以滿(mǎn)足數(shù)據(jù)訪問(wèn)一致性的需求。2004年,Java5.0開(kāi)始引入適用于多線程環(huán)境的內(nèi)存模型,而C++直到C++11才開(kāi)始引入。

          **Herb Sutter**在其文章中這樣來(lái)評(píng)價(jià)C++11引入的內(nèi)存模型:

          The memory model means that C++ code now has a standardized library to call regardless of who made the compiler and on what platform it's running. There's a standard way to control how different threads talk to the processor's memory.

          "When you are talking about splitting [code] across different cores that's in the standard, we are talking about the memory model. We are going to optimize it without breaking the following assumptions people are going to make in the code," Sutter said

          從內(nèi)容可以看出,C++11引入Memory model的意義在于有了一個(gè)語(yǔ)言層面的、與運(yùn)行平臺(tái)和編譯器無(wú)關(guān)的標(biāo)準(zhǔn)庫(kù),可以使得開(kāi)發(fā)人員更為便捷高效地控制內(nèi)存訪問(wèn)順序。

          一言以蔽之,引入內(nèi)存模型的原因,有以下幾個(gè)原因:

          • ? 編譯器優(yōu)化:在某些情況下,即使是簡(jiǎn)單的語(yǔ)句,也不能保證是原子操作

          • ? CPU out-of-order:CPU為了提升計(jì)算性能,可能會(huì)調(diào)整指令的執(zhí)行順序

          • ? CPU Cache不一致:在CPU Cache的影響下,在某個(gè)CPU下執(zhí)行了指令,不會(huì)立即被其它CPU所看到

          關(guān)系術(shù)語(yǔ)

          為了便于更好地理解后面的內(nèi)容,我們需要理解幾種關(guān)系術(shù)語(yǔ)。

          sequenced-before

          sequenced-before是一種單線程上的關(guān)系,這是一個(gè)非對(duì)稱(chēng),可傳遞的成對(duì)關(guān)系。

          在了解sequenced-before之前,我們需要先看一個(gè)概念evaluation(求值)

          對(duì)一個(gè)表達(dá)式進(jìn)行求值(evaluation),包含以下兩部分:

          • value computations: calculation of the value that is returned by the expression. This may involve determination of the identity of the object (glvalue evaluation, e.g. if the expression returns a reference to some object) or reading the value previously assigned to an object (prvalue evaluation, e.g. if the expression returns a number, or some other value)

          • ? Initiation of side effects: access (read or write) to an object designated by a volatile glvalue, modification (writing) to an object, calling a library I/O function, or calling a function that does any of those operations.

          上述內(nèi)容簡(jiǎn)單理解就是,value computation就是計(jì)算表達(dá)式的值,side effect就是對(duì)對(duì)象進(jìn)行讀寫(xiě)。

          對(duì)于C++來(lái)說(shuō),語(yǔ)言本身并沒(méi)有規(guī)定表達(dá)式的求值順序,因此像是f1() + f2() + f3()這種表達(dá)式,編譯器可以決定先執(zhí)行哪個(gè)函數(shù),之后再按照加法運(yùn)算的規(guī)則從左邊加到右邊,因此編譯器可能會(huì)優(yōu)化成為(f1() + f2()) + f(3),但f1() + f2()和f3()都可以先執(zhí)行。

          經(jīng)常可以看到如下這種代碼:

          i = i++ + i;

          正是因?yàn)檎Z(yǔ)言本身沒(méi)有規(guī)定表達(dá)式的求值順序,所以上述代碼中兩個(gè)子表達(dá)式(i++和i)無(wú)法確定先后順序,因此這個(gè)語(yǔ)句的行為是未定義的。

          sequenced-before就是對(duì)在同一個(gè)線程內(nèi),求值順序關(guān)系的描述:

          • ? 如果A sequenced-before B,代表A的求值會(huì)先完成,才進(jìn)行對(duì)B的求值

          • ? 如果A not sequenced-before B,而B(niǎo) sequenced-before A,則代表先對(duì)B進(jìn)行求值,然后對(duì)A進(jìn)行求值

          • ? 如果A not sequenced-before B,而B(niǎo) not sequenced-before A,則A和B都有可能先執(zhí)行,甚至可以同時(shí)執(zhí)行

          happens-before

          happens-before是sequenced-before的擴(kuò)展,因?yàn)樗€包含了不同線程之間的關(guān)系。當(dāng)A操作happens-before B操作的時(shí)候,操作A先于操作B執(zhí)行,且A操作的結(jié)果對(duì)B來(lái)說(shuō)可見(jiàn)。

          看下cppreference對(duì)happens-before關(guān)系的定義,如下:

          Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:

          1) A is sequenced-before B

          2) A inter-thread happens before B

          從上述定義可以看出,happens-before包含兩種情況,一種是同一線程內(nèi)的happens-before關(guān)系(等同于sequenced-before),另一種是不同線程的happens-before關(guān)系。

          對(duì)于同一線程內(nèi)的happens-before,其等同于sequenced-before,所以在此忽略,著重講下線程間的happens-before關(guān)系

          假設(shè)有一個(gè)變量x,其初始化為0,如下:

          int x = 0;

          此時(shí)有兩個(gè)線程同時(shí)運(yùn)行,線程A進(jìn)行++x操作,線程B打印x的值。因?yàn)檫@兩個(gè)線程不具備happens-before關(guān)系,也就是說(shuō)沒(méi)有保證++x操作對(duì)于打印x的操作是可見(jiàn)的,因此打印的值有可能是0,也有可能是1。

          對(duì)于這種場(chǎng)景,語(yǔ)言本身必須提供適當(dāng)?shù)氖侄危梢允沟瞄_(kāi)發(fā)人員能夠在多線程場(chǎng)景下達(dá)到happens-before的關(guān)系,進(jìn)而得到正確的運(yùn)行結(jié)果。這也就是上面說(shuō)的第二點(diǎn)inter-thread happens before B

          C++中定義了5種能夠建立跨線程的happens-before的場(chǎng)景,如下:

          • ? A synchronizes-with B

          • ? A is dependency-ordered before B

          • ? A synchronizes-with some evaluation X, and X is sequenced-before B

          • ? A is sequenced-before some evaluation X, and X inter-thread happens-before B

          • ? A inter-thread happens-before some evaluation X, and X inter-thread happens-before B

          synchronizes-with

          synchronized-with描述的是不同線程間的同步關(guān)系,當(dāng)線程A synchronized-with線程B的時(shí),代表線程A對(duì)某個(gè)變量或者內(nèi)存的操作,對(duì)于線程B是可見(jiàn)的。換句話說(shuō),synchronized-with就是跨線程版本的happens-before

          假設(shè)在多線程環(huán)境下,線程A對(duì)變量x進(jìn)行x = 1的寫(xiě)操作,線程B讀取x的值。在未進(jìn)行任何同步的條件下,即使線程A先執(zhí)行,線程B后執(zhí)行,線程B讀取到的x的值也不一定是最新的值。這是因?yàn)闉榱俗尦绦驁?zhí)行效率更高編譯器或者CPU做了指令亂序優(yōu)化,也有可能A線程修改后的值在寄存器內(nèi),或者被存儲(chǔ)在CPU cache中,還沒(méi)來(lái)得及寫(xiě)入內(nèi)存 。正是因?yàn)榉N種操作 ,所以在多線程環(huán)境下,假如同時(shí)存在讀寫(xiě)操作,就需要對(duì)該變量或者內(nèi)存做同步操作。

          所以,synchronizes-with是這樣一種關(guān)系,它可以保證線程A的寫(xiě)操作結(jié)果,在線程B是可見(jiàn)的。

          在2014年C++的官方標(biāo)準(zhǔn)文件(Standard for Programming Language C++)N4296的第12頁(yè),提示了C++提供的同步操作,也就是使用atomic或mutex:

          The library defines a number of atomic operations and operations on mutexes that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another.

          memory_order

          C++11中引入了六種內(nèi)存約束符用以解決多線程下的內(nèi)存一致性問(wèn)題(在頭文件中),其定義如下:

          typedef enum memory_order {
              memory_order_relaxed,
              memory_order_consume,
              memory_order_acquire,
              memory_order_release,
              memory_order_acq_rel,
              memory_order_seq_cst
          } memory_order;

          這六種內(nèi)存約束符從讀/寫(xiě)的角度進(jìn)行劃分的話,可以分為以下三種:

          • ? 讀操作(memory_order_acquire memory_order_consume)

          • ? 寫(xiě)操作(memory_order_release)

          • ? 讀-修改-寫(xiě)操作(memory_order_acq_rel memory_order_seq_cst)

          ps: 因?yàn)閙emory_order_relaxed沒(méi)有定義同步和排序約束,所以它不適合這個(gè)分類(lèi)。

          舉例來(lái)說(shuō),因?yàn)閟tore是一個(gè)寫(xiě)操作,當(dāng)調(diào)用store時(shí),指定memory_order_relaxed或者memory_order_release或者memory_order_seq_cst是有意義的。而指定memory_order_acquire是沒(méi)有意義的。

          從訪問(wèn)控制的角度可以分為以下三種:

          • ? Sequential consistency模型(memory_order_seq_cst)

          • ? Relax模型(memory_order_relaxed)

          • ? Acquire-Release模型(memory_order_consume memory_order_acquire memory_order_release memory_order_acq_rel)

          從從訪問(wèn)控制的強(qiáng)弱排序,Sequential consistency模型最強(qiáng),Acquire-Release模型次之,Relax模型最弱。

          在后面的內(nèi)容中,將結(jié)合這6種約束符來(lái)進(jìn)一步分析內(nèi)存模型。

          內(nèi)存模型

          Sequential consistency模型

          Sequential consistency模型又稱(chēng)為順序一致性模型,是控制粒度最嚴(yán)格的內(nèi)存模型。最早追溯到Leslie Lamport在19799月發(fā)表的論文《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》,在該文里面首次提出了Sequential consistency概念:

          the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program

          根據(jù)這個(gè)定義,在順序一致性模型下,程序的執(zhí)行順序與代碼順序嚴(yán)格一致,也就是說(shuō),在順序一致性模型中,不存在指令亂序。

          順序一致性模型對(duì)應(yīng)的約束符號(hào)是memory_order_seq_cst,這個(gè)模型對(duì)于內(nèi)存訪問(wèn)順序的一致性控制是最強(qiáng)的,類(lèi)似于很容易理解的互斥鎖模式,先得到鎖的先訪問(wèn)。

          假設(shè)有兩個(gè)線程,分別是線程A和線程B,那么這兩個(gè)線程的執(zhí)行情況有三種:第一種是線程A先執(zhí)行,然后再執(zhí)行線程B;第二種情況是線程 B 先執(zhí)行,然后再執(zhí)行線程A;第三種情況是線程A和線程B同時(shí)并發(fā)執(zhí)行,即線程A的代碼序列和線程B的代碼序列交替執(zhí)行。盡管可能存在第三種代碼交替執(zhí)行的情況,但是單純從線程A或線程B的角度來(lái)看,每個(gè)線程的代碼執(zhí)行應(yīng)該是按照代碼順序執(zhí)行的,這就順序一致性模型。總結(jié)起來(lái)就是:

          • ? 每個(gè)線程的執(zhí)行順序與代碼順序嚴(yán)格一致

          • ? 線程的執(zhí)行順序可能會(huì)交替進(jìn)行,但是從單個(gè)線程的角度來(lái)看,仍然是順序執(zhí)行

          為了便于理解上述內(nèi)容,舉例如下:

          x = y = 0;

          thread1:
          x = 1;
          r1 = y;

          thread2:
          y = 1;
          r2 = x;

          因?yàn)槎嗑€程執(zhí)行順序有可能是交錯(cuò)執(zhí)行的,所以上述示例執(zhí)行順序有可能是:

          • ? x = 1; r1 = y; y = 1; r2 = x

          • ? y = 1; r2 = x; x = 1; r1 = y

          • ? x = 1; y = 1; r1 = y; r2 = x

          • ? x = 1; r2 = x; y = 1; r1 = y

          • ? y = 1; x = 1; r1 = y; r2 = x

          • ? y = 1; x = 1; r2 = x; r1 = y

          也就是說(shuō),雖然多線程環(huán)境下,執(zhí)行順序是亂的,但是單純從線程1的角度來(lái)看,執(zhí)行順序是x = 1; r1 = y;從線程2角度來(lái)看,執(zhí)行順序是y = 1; r2 = x

          std::atomic的操作都使用memory_order_seq_cst 作為默認(rèn)值。如果不確定使用何種內(nèi)存訪問(wèn)模型,用 memory_order_seq_cst能確保不出錯(cuò)。

          順序一致性的所有操作都按照代碼指定的順序進(jìn)行,符合開(kāi)發(fā)人員的思維邏輯,但這種嚴(yán)格的排序也限制了現(xiàn)代CPU利用硬件進(jìn)行并行處理的能力,會(huì)嚴(yán)重拖累系統(tǒng)的性能。

          Relax模型

          Relax模型對(duì)應(yīng)的是memory_order中的memory_order_relaxed。從其字面意思就能看出,其對(duì)于內(nèi)存序的限制最小,也就是說(shuō)這種方式只能保證當(dāng)前的數(shù)據(jù)訪問(wèn)是原子操作(不會(huì)被其他線程的操作打斷),但是對(duì)內(nèi)存訪問(wèn)順序沒(méi)有任何約束,也就是說(shuō)對(duì)不同的數(shù)據(jù)的讀寫(xiě)可能會(huì)被重新排序。

          為了便于理解Relax模型,我們舉一個(gè)簡(jiǎn)單的例子,代碼如下:

          #include <atomic>
          #include <thread>
          #include <iostream>

          std::atomic<bool> x{false};
          int a = 0;

          void fun1() { // 線程1
            a = 1// L9
            x.store(true, std::memory_order_relaxed); // L10
          }
          void func2() { // 線程2
            while(!x.load(std::memory_order_relaxed)); // L13
            if(a) { // L14
              std::cout << "a = 1" << std::endl;
            }
          }
          int main() {
            std::thread t1(fun1);
            std::thread t2(fun2);
            t1.join();
            t2.join();
            return 0;
          }

          上述代碼中,線程1有兩個(gè)代碼語(yǔ)句,語(yǔ)句L9是一個(gè)簡(jiǎn)單的賦值操作,語(yǔ)句L10是一個(gè)帶有memory_order_relaxed標(biāo)記的原子寫(xiě)操作,基于reorder原則,這兩句的順序沒(méi)有確定下即不能保證哪個(gè)在前,哪個(gè)在后。而對(duì)于線程2,也有兩個(gè)代碼句,分別是帶有memory_order_relaxed標(biāo)記的原子讀操作L13和簡(jiǎn)單的判斷輸出語(yǔ)句L14。需要注意的是語(yǔ)句L13和語(yǔ)句L14的順序是確定的,即語(yǔ)句L13 happens-before 語(yǔ)句L14,這是由while循環(huán)代碼語(yǔ)義保證的。換句話說(shuō),while語(yǔ)句優(yōu)先于后面的語(yǔ)句執(zhí)行,這是編譯器或者CPU的重排規(guī)則。

          對(duì)于上述示例,我們第一印象會(huì)輸出a = 1 這句。但實(shí)際上,也有可能不會(huì)輸出。這是因?yàn)樵诰€程1中,因?yàn)橹噶畹膩y序重排,有可能導(dǎo)致L10先執(zhí)行,然后再執(zhí)行語(yǔ)句L9。如果結(jié)合了線程2一起來(lái)分析,就是這4個(gè)代碼句的執(zhí)行順序有可能是#L10-->L13-->L14-->L9,這樣就不能得到我們想要的結(jié)果了。

          那么既然memory_order_relaxed不能保證執(zhí)行順序,它們的使用場(chǎng)景又是什么呢?這就需要用到其特性即只保證當(dāng)前的數(shù)據(jù)訪問(wèn)是原子操作,通常用于一些統(tǒng)計(jì)計(jì)數(shù)的需求場(chǎng)景,代碼如下:

          #include <cassert>
          #include <vector>
          #include <iostream>
          #include <thread>
          #include <atomic>
          std::atomic<int> cnt = {0};
          void fun1() {
            for (int n = 0; n < 100; ++n) {
              cnt.fetch_add(1, std::memory_order_relaxed);
            }
          }

          void fun2() {
            for (int n = 0; n < 900; ++n) {
              cnt.fetch_add(1, std::memory_order_relaxed);
            }
          }

          int main() {
            std::thread t1(fun1);
            std::thread t2(fun2);
            t1.join();
            t2.join();
            
            return 0;
          }

          在上述代碼執(zhí)行完成后,cnt == 1000。

          通常,與其它內(nèi)存序相比,寬松內(nèi)存序具有最少的同步開(kāi)銷(xiāo)。但是,正因?yàn)橥介_(kāi)銷(xiāo)小,這就導(dǎo)致了不確定性,所以我們?cè)陂_(kāi)發(fā)過(guò)程中,根據(jù)自己的使用場(chǎng)景來(lái)選擇合適的內(nèi)存序選項(xiàng)。

          Acquire-Release模型

          Acquire-Release模型的控制力度介于Relax模型和Sequential consistency模型之間。其定義如下:

          • ? Acquire:如果一個(gè)操作X帶有acquire語(yǔ)義,那么在操作X后的所有讀寫(xiě)指令都不會(huì)被重排序到操作X之前

          • ? Relase:如果一個(gè)操作X帶有release語(yǔ)義,那么在操作X前的所有讀寫(xiě)指令操作都不會(huì)被重排序到操作X之后

          結(jié)合上面的定義,重新解釋下該模型:假設(shè)有一個(gè)原子變量A,對(duì)A的寫(xiě)操作(Release)和讀操作(Acquire)之間進(jìn)行同步,并建立排序約束關(guān)系,即對(duì)于寫(xiě)操作(release)X,在寫(xiě)操作X之前的所有讀寫(xiě)指令都不能放到寫(xiě)操作X之后;對(duì)于讀操作(acquire)Y,在讀操作Y之后的所有讀寫(xiě)指令都不能放到讀操作Y之前。

          Acquire-Release模型對(duì)應(yīng)六種約束關(guān)系中的memory_order_consume、memory_order_acquire、memory_order_release和memory_order_acq_rel。這些約束關(guān)系,有的只能用于讀操作(memory_order_consume、memory_order_acquire),有的適用于寫(xiě)操作(memory_order_release),有的技能用于讀操作也能用于寫(xiě)操作(memory_order_acq_rel)。這些約束符互相配合,可以實(shí)現(xiàn)相對(duì)嚴(yán)格一點(diǎn)的內(nèi)存訪問(wèn)順序控制。

          memory_order_release

          假設(shè)有一個(gè)原子變量A,對(duì)其進(jìn)行寫(xiě)操作X的時(shí)候施加了memory_order_release約束符,則在當(dāng)前線程T1中,該操作X之前的任何讀寫(xiě)操作指令都不能放在操作X之后。當(dāng)另外一個(gè)線程T2對(duì)原子變量A進(jìn)行讀操作的時(shí)候,施加了memory_order_acquire約束符,則當(dāng)前線程T1中寫(xiě)操作之前的任何讀寫(xiě)操作都對(duì)線程T2可見(jiàn);當(dāng)另外一個(gè)線程T2對(duì)原子變量A進(jìn)行讀操作的時(shí)候,如果施加了memory_order_consume約束符,則當(dāng)前線程T1中所有原子變量A所依賴(lài)的讀寫(xiě)操作都對(duì)T2線程可見(jiàn)(沒(méi)有依賴(lài)關(guān)系的內(nèi)存操作就不能保證順序)。

          需要注意的是,對(duì)于施加了memory_order_release約束符的寫(xiě)操作,其寫(xiě)之前所有讀寫(xiě)指令操作都不會(huì)被重排序?qū)懖僮髦蟮那疤崾牵?code style="white-space:pre-wrap;text-align: left;line-height: 1.75;font-size: 14.4px;color: rgb(221, 17, 68);background: rgba(27, 31, 35, 0.05);padding: 3px 5px;border-radius: 4px;">其他線程對(duì)這個(gè)原子變量執(zhí)行了讀操作,且施加了memory_order_acquire或者 memory_order_consume約束符。

          memory_order_acquire

          一個(gè)對(duì)原子變量的load操作時(shí),使用memory_order_acquire約束符:在當(dāng)前線程中,該load之后讀和寫(xiě)操作都不能被重排到當(dāng)前指令前。如果其他線程使用memory_order_release約束符,則對(duì)此原子變量進(jìn)行store操作,在當(dāng)前線程中是可見(jiàn)的。

          假設(shè)有一個(gè)原子變量A,如果A的讀操作X施加了memory_order_acquire標(biāo)記,則在當(dāng)前線程T1中,在操作X之后的所有讀寫(xiě)指令都不能重排到操作X之前;當(dāng)其它線程如果對(duì)A進(jìn)行施加了memory_order_release約束符的寫(xiě)操作Y,則這個(gè)寫(xiě)操作Y之前所有的讀寫(xiě)指令對(duì)當(dāng)前線程T1是可見(jiàn)的(這里的可見(jiàn)請(qǐng)結(jié)合 happens-before 原則理解,即那些內(nèi)存讀寫(xiě)操作會(huì)確保完成,不會(huì)被重新排序)。也就是說(shuō)從線程T2的角度來(lái)看,在原子變量A寫(xiě)操作之前發(fā)生的所有內(nèi)存寫(xiě)入在線程T1中都會(huì)產(chǎn)生作用。也就是說(shuō),一旦原子讀取完成,線程T1就可以保證看到線程 A 寫(xiě)入內(nèi)存的所有內(nèi)容。

          為了便于理解,使用cppreference中的例子,如下:

          #include <thread>
          #include <atomic>
          #include <cassert>
          #include <string>
           
          std::atomic<std::string*> ptr;
          int data;
           
          void producer() {
            std::string* p  = new std::string("Hello");  // L10
            data = 42// L11
            ptr.store(p, std::memory_order_release); // L12
          }
           
          void consumer() {
            std::string* p2;
            while (!(p2 = ptr.load(std::memory_order_acquire))); // L17
            assert(*p2 == "Hello"); // L18
            assert(data == 42); // L19
          }
           
          int main() {
            std::thread t1(producer);
            std::thread t2(consumer);
            t1.join(); 
            t2.join();
            
            return 0;
          }

          在上述例子中,原子變量ptr的寫(xiě)操作(L12)施加了memory_order_release標(biāo)記,根據(jù)前面所講,這意味著在線程producer中,L10和L11不會(huì)重排到L12之后;在consumer線程中,對(duì)原子變量ptr的讀操作L17施加了memory_order_acquire標(biāo)記,也就是說(shuō)L8和L19不會(huì)重排到L17之前,這也就意味著當(dāng)L17讀到的ptr不為null的時(shí)候,producer線程中的L10和L11操作對(duì)consumer線程是可見(jiàn)的,因此consumer線程中的assert是成立的。

          memory_order_consume

          一個(gè)load操作使用了memory_order_consume約束符:在當(dāng)前線程中,load操作之后的依賴(lài)于此原子變量的讀和寫(xiě)操作都不能被重排到當(dāng)前指令前。如果有其他線程使用memory_order_release內(nèi)存模型對(duì)此原子變量進(jìn)行store操作,在當(dāng)前線程中是可見(jiàn)的。

          在理解memory_order_consume約束符的意義之前,我們先了解下依賴(lài)關(guān)系,舉例如下:

          std::atomic<std::string*> ptr;
          int data;

          std::string* p  =newstd::string("Hello");
          data =42;                                   
          ptr.store(p,std::memory_order_release);

          在該示例中,原子變量ptr依賴(lài)于p,但是不依賴(lài)data,而p和data互不依賴(lài)

          現(xiàn)在結(jié)合依賴(lài)關(guān)系,理解下memory_order_consume標(biāo)記的意義:有一個(gè)原子變量A,在線程T1中對(duì)原子變量的寫(xiě)操作施加了memory_order_release標(biāo)記符,同時(shí)線程T2對(duì)原子變量A的讀操作被標(biāo)記為memory_order_consume,則從線程T1的角度來(lái)看,在原子變量寫(xiě)之前發(fā)生的所有讀寫(xiě)操作,只有與該變量有依賴(lài)關(guān)系的內(nèi)存讀寫(xiě)才會(huì)保證不會(huì)重排到這個(gè)寫(xiě)操作之后,也就是說(shuō),當(dāng)線程T2使用了帶memory_order_consume標(biāo)記的讀操作時(shí),線程T1中只有與這個(gè)原子變量有依賴(lài)關(guān)系的讀寫(xiě)操作才不會(huì)被重排到寫(xiě)操作之后。而如果讀操作施加了memory_order_acquire標(biāo)記,則線程T1中所有寫(xiě)操作之前的讀寫(xiě)操作都不會(huì)重排到寫(xiě)之后(此處需要注意的是,一個(gè)是有依賴(lài)關(guān)系的不重排,一個(gè)是全部不重排)。

          同樣,使用cppreference中的例子,如下:

          #include <thread>
          #include <atomic>
          #include <cassert>
          #include <string>
           
          std::atomic<std::string*> ptr;
          int data;
           
          void producer() {
            std::string* p  = new std::string("Hello"); // L10
            data = 42// L11
            ptr.store(p, std::memory_order_release); // L12
          }
           
          void consumer() {
            std::string* p2;
            while (!(p2 = ptr.load(std::memory_order_consume))); // L17
            assert(*p2 == "Hello"); // L18
            assert(data == 42); // L19
          }
           
          int main() {
            std::thread t1(producer);
            std::thread t2(consumer);
            t1.join(); 
            t2.join();
            
            return 0;
          }

          與memory_order_acquire一節(jié)中示例相比較,producer()沒(méi)有變化,consumer()函數(shù)中將load操作的標(biāo)記符從memory_order_acquire變成了memory_order_consume。而這個(gè)變動(dòng)會(huì)引起如下變化:producer()中,ptr與p有依賴(lài) 關(guān)系,則p不會(huì)重排到store()操作L12之后,而data因?yàn)榕cptr沒(méi)有依賴(lài)關(guān)系,則可能重排到L12之后,所以可能導(dǎo)致L19的assert()失敗。

          截止到此,分析了memory_order_acquire&memory_order_acquire組合以及memory_order_release&memory_order_consume組合的對(duì)重排的影響:當(dāng)對(duì)讀操作使用memory_order_acquire標(biāo)記的時(shí)候,對(duì)于寫(xiě)操作來(lái)說(shuō),寫(xiě)操作之前的所有讀寫(xiě)都不能重排到寫(xiě)操作之后,對(duì)于讀操作來(lái)說(shuō),讀操作之后的所有讀寫(xiě)不能重排到讀操作之前;當(dāng)讀操作使用memory_order_consume標(biāo)記的時(shí)候,對(duì)于寫(xiě)操作來(lái)說(shuō),與原子變量有依賴(lài)關(guān)系的所有讀寫(xiě)操作都不能重排到寫(xiě)操作之后,對(duì)于讀操作來(lái)說(shuō),當(dāng)前線程中任何與這個(gè)讀取操作有依賴(lài)關(guān)系的讀寫(xiě)操作都不會(huì)被重排到當(dāng)前讀取操作之前。

          當(dāng)對(duì)一個(gè)原子變量的讀操作施加了memory_order_acquire標(biāo)記時(shí),對(duì)那些使用 memory_order_release標(biāo)記的寫(xiě)操作線程來(lái)說(shuō),這些線程中在寫(xiě)之前的所有內(nèi)存操作都不能被重排到寫(xiě)操作之后,這將嚴(yán)重限制 CPU 和編譯器優(yōu)化代碼執(zhí)行的能力。所以,當(dāng)確定只需對(duì)某個(gè)變量限制訪問(wèn)順序的時(shí)候,應(yīng)盡量使用 memory_order_consume,減少代碼重排的限制,以提升程序性能。

          memory_order_consume約束符是對(duì)acquire&release語(yǔ)義的一種優(yōu)化,這種優(yōu)化僅限定于與原子變量存在依賴(lài)關(guān)系的變量操作,因此在重新排序的限制上,其比memory_order_acquire更為寬容。需要注意的是,因?yàn)閙emory_order_consume實(shí)現(xiàn)的復(fù)雜性,自2016年6月起,所有的編譯器的實(shí)現(xiàn)中,memory_order_consume和memory_order_acquire的功能完全一致,詳見(jiàn)《P0371R1: Temporarily discourage memory_order_consume》

          memory_order_acq_rel

          Acquire-Release模型中的其它三個(gè)約束符,要么用來(lái)約束讀,要么用來(lái)約束寫(xiě)。那么如何對(duì)一個(gè)原子操作中的兩個(gè)動(dòng)作執(zhí)行約束呢?這就要用到 memory_order_acq_rel,它既可以約束讀,也可以約束寫(xiě)。

          對(duì)于使用memory_order_acq_rel約束符的原子操作,對(duì)當(dāng)前線程的影響就是:當(dāng)前線程T1中此操作之前或者之后的內(nèi)存讀寫(xiě)都不能被重新排序(假設(shè)此操作之前的操作為操作A,此操作為操作B,此操作之后的操作為B,那么執(zhí)行順序總是ABC,這塊可以理解為同一線程內(nèi)的sequenced-before關(guān)系);對(duì)其它線程T2的影響是,如果T2線程使用了memory_order_release約束符的寫(xiě)操作,那么T2線程中寫(xiě)操作之前的所有操作均對(duì)T1線程可見(jiàn);如果T2線程使用了memory_order_acquire約束符的讀操作,則T1線程的寫(xiě)操作對(duì)T2線程可見(jiàn)。

          理解起來(lái)可能比較繞,這個(gè)標(biāo)記相當(dāng)于對(duì)讀操作使用了memory_order_acquire約束符,對(duì)寫(xiě)操作使用了memory_order_release約束符。當(dāng)前線程中這個(gè)操作之前的內(nèi)存讀寫(xiě)不能被重排到這個(gè)操作之后,這個(gè)操作之后的內(nèi)存讀寫(xiě)也不能被重排到這個(gè)操作之前。

          cppreference中使用了3個(gè)線程的例子來(lái)解釋memory_order_acq_rel約束符,代碼如下:

          #include <thread>
          #include <atomic>
          #include <cassert>
          #include <vector>
           
          std::vector<int> data;
          std::atomic<int> flag = {0};
           
          void thread_1() {
              data.push_back(42); // L10
              flag.store(1, std::memory_order_release); // L11
          }
           
          void thread_2() {
              int expected=1// L15
              // memory_order_relaxed is okay because this is an RMW,
              // and RMWs (with any ordering) following a release form a release sequence
              while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) { // L18
                  expected = 1;
              }
          }
           
          void thread_3() {
              while (flag.load(std::memory_order_acquire) < 2); // L24
              // if we read the value 2 from the atomic flag, we see 42 in the vector
              assert(data.at(0) == 42); // L26
          }
           
          int main() {
              std::thread a(thread_1);
              std::thread b(thread_2);
              std::thread c(thread_3);
              a.join(); 
              b.join(); 
              c.join();
              
              return 0;
          }

          線程thread_2中,對(duì)原子變量flag的compare_exchange操作使用了memory_order_acq_rel約束符,這就意味著L15不能重排到L18之后,也就是說(shuō)當(dāng)compare_exchange操作發(fā)生的時(shí)候,能確保expected的值是1,使得這個(gè) compare_exchange_strong操作能夠完成將flag替換成2的動(dòng)作;thread_1線程中對(duì)flag使用了帶memory_order_release約束符的store,這意味著當(dāng)thread_2線程中取flag的值的時(shí)候,L10已經(jīng)完成(不會(huì)被重排到L11之后)。當(dāng)thread_2線程compare_exchange操作將2寫(xiě)入flag的時(shí)候,thread_3線程中帶memory_order_acquire標(biāo)記的load操作能看到L18之前的內(nèi)存寫(xiě)入,自然也包括L10的內(nèi)存寫(xiě)入,所以L26的斷言始終是成立的。

          上面例子中,memory_order_acq_rel約束符用于同時(shí)存在讀和寫(xiě)的場(chǎng)景,這個(gè)時(shí)候,相當(dāng)于使用了memory_order_acquire&memory_order_acquire組合組合。其實(shí),它也可以單獨(dú)用于讀或者單獨(dú)用于寫(xiě),示例如下:

          // Thread-1:
          a = y.load(memory_order_acq_rel); // A
          x.store(a, memory_order_acq_rel); // B

          // Thread-2:
          b = x.load(memory_order_acq_rel); // C
          y.store(1, memory_order_acq_rel); // D

          另外一個(gè)實(shí)例:

          // Thread-1:                              
          a = y.load(memory_order_acquire); // A
          x.store(a, memory_order_release); // B

          // Thread-2:
          b = x.load(memory_order_acquire); // C
          y.store(1, memory_order_release); // D

          上述兩個(gè)示例,效果完全一樣,都可以保證A先于B執(zhí)行,C先于D執(zhí)行。

          總結(jié)

          C++11提供的6種內(nèi)存訪問(wèn)約束符中:

          • ? memory_order_release:在當(dāng)前線程T1中,該操作X之前的任何讀寫(xiě)操作指令都不能放在操作X之后。如果其它線程對(duì)同一變量使用了memory_order_acquire或者memory_order_consume約束符,則當(dāng)前線程寫(xiě)操作之前的任何讀寫(xiě)操作都對(duì)其它線程可見(jiàn)(注意consume的話是依賴(lài)關(guān)系可見(jiàn))

          • ? memory_order_acquire:在當(dāng)前線程中,load操作之后的讀和寫(xiě)操作都不能被重排到當(dāng)前指令前。如果有其他線程使用memory_order_release內(nèi)存模型對(duì)此原子變量進(jìn)行store操作,在當(dāng)前線程中是可見(jiàn)的。

          • ? memory_order_relaxed:沒(méi)有同步或順序制約,僅對(duì)此操作要求原子性

          • ? memory_order_consume:在當(dāng)前線程中,load操作之后的依賴(lài)于此原子變量的讀和寫(xiě)操作都不能被重排到當(dāng)前指令前。如果有其他線程使用memory_order_release內(nèi)存模型對(duì)此原子變量進(jìn)行store操作,在當(dāng)前線程中是可見(jiàn)的。

          • ? memory_order_acq_rel:等同于對(duì)原子變量同時(shí)使用memory_order_release和memory_order_acquire約束符

          • ? memory_order_seq_cst:從宏觀角度看,線程的執(zhí)行順序與代碼順序嚴(yán)格一致

          C++的內(nèi)存模型則是依賴(lài)上面六種內(nèi)存約束符來(lái)實(shí)現(xiàn)的:

          • ? Relax模型:對(duì)應(yīng)的是memory_order中的memory_order_relaxed。從其字面意思就能看出,其對(duì)于內(nèi)存序的限制最小,也就是說(shuō)這種方式只能保證當(dāng)前的數(shù)據(jù)訪問(wèn)是原子操作(不會(huì)被其他線程的操作打斷),但是對(duì)內(nèi)存訪問(wèn)順序沒(méi)有任何約束,也就是說(shuō)對(duì)不同的數(shù)據(jù)的讀寫(xiě)可能會(huì)被重新排序

          • ? Acquire-Release模型:對(duì)應(yīng)的memory_order_consume memory_order_acquire memory_order_release memory_order_acq_rel約束符(需要互相配合使用);對(duì)于一個(gè)原子變量A,對(duì)A的寫(xiě)操作(Release)和讀操作(Acquire)之間進(jìn)行同步,并建立排序約束關(guān)系,即對(duì)于寫(xiě)操作(release)X,在寫(xiě)操作X之前的所有讀寫(xiě)指令都不能放到寫(xiě)操作X之后;對(duì)于讀操作(acquire)Y,在讀操作Y之后的所有讀寫(xiě)指令都不能放到讀操作Y之前。

          • ? Sequential consistency模型:對(duì)應(yīng)的memory_order_seq_cst約束符;程序的執(zhí)行順序與代碼順序嚴(yán)格一致,也就是說(shuō),在順序一致性模型中,不存在指令亂序。

          下面這幅圖大致梳理了內(nèi)存模型的核心概念,可以幫我們快速回顧。

          后記

          這篇文章斷斷續(xù)續(xù)寫(xiě)了一個(gè)多月,中間很多次都想放棄。不過(guò),幸好還是咬牙堅(jiān)持了下來(lái)。查了很多資料,奈何因?yàn)橹R(shí)儲(chǔ)備不足,很多地方都沒(méi)有理解透徹,所以文章中可能存在理解偏差,希望友好交流,共同進(jìn)步。

          在寫(xiě)文的過(guò)程中,深切體會(huì)到了內(nèi)存模型的復(fù)雜高深之處,C++的內(nèi)存模型為了提供足夠的靈活性和高性能,將各種約束符都暴露給了開(kāi)發(fā)人員,給高手足夠的發(fā)揮空間,也讓新手一臉茫然。

          好了,今天的文章就到這,我們下期見(jiàn)!

          瀏覽 27
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费高清无码视频在线观看 | 想操逼| 欧美三级片视频网站 | 青娱乐在线观看网址 | 国产一级婬乱A片免费 |