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

當(dāng)前位置:首頁 > 單片機 > 程序喵大人

寫在前面

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

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

指令亂序

現(xiàn)在的CPU都采用的是多核、多線程技術(shù)用以提升計算能力;采用亂序執(zhí)行、流水線、分支預(yù)測以及多級緩存等方法來提升程序性能。多核技術(shù)在提升程序性能的同時,也帶來了執(zhí)行序列亂序和內(nèi)存序列訪問的亂序問題。與此同時,編譯器也會基于自己的規(guī)則對代碼進行優(yōu)化,這些優(yōu)化動作也會導(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)

通過上述指令,可以看到,先把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賦值(語句L6)語句先于A賦值語句(語句L5)執(zhí)行。

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

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

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

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

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

類似的,處理器也會提供指令給開發(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,提升計算能力。在單核時代,多線程的概念是在宏觀上并行,微觀上串行,多線程可以訪問相同的CPU緩存和同一組寄存器。但是在多核時代,多個線程可能執(zhí)行在不同的核上,每個CPU都有自己的緩存和寄存器,在一個CPU上執(zhí)行的線程無法訪問另一個CPU的緩存和寄存器。CPU會根據(jù)一定的規(guī)則對機器指令的內(nèi)存交互進行重新排序,特別是允許每個處理器延遲存儲并且從不同位置裝載數(shù)據(jù)。與此同時,編譯器也會基于自己的規(guī)則對代碼進行優(yōu)化,這些優(yōu)化動作也會導(dǎo)致一些代碼的順序被重排。這種指令的重排,雖然不影響單線程的執(zhí)行結(jié)果,但是會加劇多線程訪問共享數(shù)據(jù)時的數(shù)據(jù)競爭(Data Race)問題。

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

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

**Herb Sutter**在其文章中這樣來評價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的意義在于有了一個語言層面的、與運行平臺和編譯器無關(guān)的標準庫,可以使得開發(fā)人員更為便捷高效地控制內(nèi)存訪問順序。

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

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

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

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

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

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

sequenced-before

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

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

對一個表達式進行求值(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)容簡單理解就是,value computation就是計算表達式的值,side effect就是對對象進行讀寫。

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

經(jīng)??梢钥吹饺缦逻@種代碼:

i = i++ + i;

正是因為語言本身沒有規(guī)定表達式的求值順序,所以上述代碼中兩個子表達式(i++和i)無法確定先后順序,因此這個語句的行為是未定義的。

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

  • ? 如果A sequenced-before B,代表A的求值會先完成,才進行對B的求值

  • ? 如果A not sequenced-before B,而B sequenced-before A,則代表先對B進行求值,然后對A進行求值

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

happens-before

happens-before是sequenced-before的擴展,因為它還包含了不同線程之間的關(guān)系。當(dāng)A操作happens-before B操作的時候,操作A先于操作B執(zhí)行,且A操作的結(jié)果對B來說可見。

看下cppreference對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)系。

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

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

int x = 0;

此時有兩個線程同時運行,線程A進行++x操作,線程B打印x的值。因為這兩個線程不具備happens-before關(guān)系,也就是說沒有保證++x操作對于打印x的操作是可見的,因此打印的值有可能是0,也有可能是1。

對于這種場景,語言本身必須提供適當(dāng)?shù)氖侄?,可以使得開發(fā)人員能夠在多線程場景下達到happens-before的關(guān)系,進而得到正確的運行結(jié)果。這也就是上面說的第二點A inter-thread happens before B。

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

  • ? 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的時,代表線程A對某個變量或者內(nèi)存的操作,對于線程B是可見的。換句話說,synchronized-with就是跨線程版本的happens-before。

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

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

在2014年C++的官方標準文件(Standard for Programming Language C++)N4296的第12頁,提示了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)存一致性問題(在頭文件中),其定義如下:

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)存約束符從讀/寫的角度進行劃分的話,可以分為以下三種:

  • ? 讀操作(memory_order_acquire memory_order_consume)

  • ? 寫操作(memory_order_release)

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

ps: 因為memory_order_relaxed沒有定義同步和排序約束,所以它不適合這個分類。

舉例來說,因為store是一個寫操作,當(dāng)調(diào)用store時,指定memory_order_relaxed或者memory_order_release或者memory_order_seq_cst是有意義的。而指定memory_order_acquire是沒有意義的。

從訪問控制的角度可以分為以下三種:

  • ? 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)

從從訪問控制的強弱排序,Sequential consistency模型最強,Acquire-Release模型次之,Relax模型最弱。

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

內(nèi)存模型

Sequential consistency模型

Sequential consistency模型又稱為順序一致性模型,是控制粒度最嚴格的內(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ù)這個定義,在順序一致性模型下,程序的執(zhí)行順序與代碼順序嚴格一致,也就是說,在順序一致性模型中,不存在指令亂序。

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

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

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

  • ? 線程的執(zhí)行順序可能會交替進行,但是從單個線程的角度來看,仍然是順序執(zhí)行

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

x = y = 0;

thread1:
x = 1;
r1 = y;

thread2:
y = 1;
r2 = x;

因為多線程執(zhí)行順序有可能是交錯執(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

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

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

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

Relax模型

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

為了便于理解Relax模型,我們舉一個簡單的例子,代碼如下:

#include  #include  #include  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有兩個代碼語句,語句L9是一個簡單的賦值操作,語句L10是一個帶有memory_order_relaxed標記的原子寫操作,基于reorder原則,這兩句的順序沒有確定下即不能保證哪個在前,哪個在后。而對于線程2,也有兩個代碼句,分別是帶有memory_order_relaxed標記的原子讀操作L13和簡單的判斷輸出語句L14。需要注意的是語句L13和語句L14的順序是確定的,即語句L13 happens-before 語句L14,這是由while循環(huán)代碼語義保證的。換句話說,while語句優(yōu)先于后面的語句執(zhí)行,這是編譯器或者CPU的重排規(guī)則。

對于上述示例,我們第一印象會輸出a = 1這句。但實際上,也有可能不會輸出。這是因為在線程1中,因為指令的亂序重排,有可能導(dǎo)致L10先執(zhí)行,然后再執(zhí)行語句L9。如果結(jié)合了線程2一起來分析,就是這4個代碼句的執(zhí)行順序有可能是#L10-->L13-->L14-->L9,這樣就不能得到我們想要的結(jié)果了。

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

#include  #include  #include  #include  #include  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)存序具有最少的同步開銷。但是,正因為同步開銷小,這就導(dǎo)致了不確定性,所以我們在開發(fā)過程中,根據(jù)自己的使用場景來選擇合適的內(nèi)存序選項。

Acquire-Release模型

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

  • ? Acquire:如果一個操作X帶有acquire語義,那么在操作X后的所有讀寫指令都不會被重排序到操作X之前

  • ? Relase:如果一個操作X帶有release語義,那么在操作X前的所有讀寫指令操作都不會被重排序到操作X之后

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

Acquire-Release模型對應(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),有的適用于寫操作(memory_order_release),有的技能用于讀操作也能用于寫操作(memory_order_acq_rel)。這些約束符互相配合,可以實現(xiàn)相對嚴格一點的內(nèi)存訪問順序控制。

memory_order_release

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

需要注意的是,對于施加了memory_order_release約束符的寫操作,其寫之前所有讀寫指令操作都不會被重排序?qū)懖僮髦蟮那疤崾牵浩渌€程對這個原子變量執(zhí)行了讀操作,且施加了memory_order_acquire或者 memory_order_consume約束符。

memory_order_acquire

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

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

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

#include  #include  #include  #include  std::atomic 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的寫操作(L12)施加了memory_order_release標記,根據(jù)前面所講,這意味著在線程producer中,L10和L11不會重排到L12之后;在consumer線程中,對原子變量ptr的讀操作L17施加了memory_order_acquire標記,也就是說L8和L19不會重排到L17之前,這也就意味著當(dāng)L17讀到的ptr不為null的時候,producer線程中的L10和L11操作對consumer線程是可見的,因此consumer線程中的assert是成立的。

memory_order_consume

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

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

std::atomic ptr; int data;

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

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

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

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

#include  #include  #include  #include  std::atomic 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()沒有變化,consumer()函數(shù)中將load操作的標記符從memory_order_acquire變成了memory_order_consume。而這個變動會引起如下變化:producer()中,ptr與p有依賴 關(guān)系,則p不會重排到store()操作L12之后,而data因為與ptr沒有依賴關(guān)系,則可能重排到L12之后,所以可能導(dǎo)致L19的assert()失敗。

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

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

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

memory_order_acq_rel

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

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

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

cppreference中使用了3個線程的例子來解釋memory_order_acq_rel約束符,代碼如下:

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

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

// 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

另外一個實例:

// 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

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

總結(jié)

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

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

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

  • ? memory_order_relaxed:沒有同步或順序制約,僅對此操作要求原子性

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

  • ? memory_order_acq_rel:等同于對原子變量同時使用memory_order_release和memory_order_acquire約束符

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

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

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

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

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

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

后記

這篇文章斷斷續(xù)續(xù)寫了一個多月,中間很多次都想放棄。不過,幸好還是咬牙堅持了下來。查了很多資料,奈何因為知識儲備不足,很多地方都沒有理解透徹,所以文章中可能存在理解偏差,希望友好交流,共同進步。

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

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