C++內(nèi)存模型和原子類型操作
std::memory_order初探

動(dòng)態(tài)內(nèi)存模型可以理解為存儲(chǔ)一致性模型,主要是從行為上來看多個(gè)線程對同一個(gè)對象讀寫操作時(shí)所做的約束,動(dòng)態(tài)內(nèi)存理解起來會(huì)有少許復(fù)雜,涉及到內(nèi)存、Cache、CPU的各個(gè)層次的交互。
如下有兩個(gè)線程,分別對a、R1、b、R2進(jìn)行賦值,根據(jù)線程執(zhí)行的順序可能有以下幾種情況

在不對線程進(jìn)行任何限制,線程內(nèi)部指令不進(jìn)行重排的情況下??梢杂?!/(2!*2!)=6中情況
在不考慮優(yōu)化和指令重排的情況下,多線程有如下兩種情況:
程序最終執(zhí)行的結(jié)果,是多個(gè)線程交織執(zhí)行的結(jié)果
從單個(gè)線程來看,該線程的指令是按照事先已經(jīng)規(guī)定好的執(zhí)行順序執(zhí)行
當(dāng)然,現(xiàn)在的編譯器都支持指令重排,上述的現(xiàn)象也只是理想情況下的執(zhí)行情況,因?yàn)轫樞蛞恢滦源鷥r(jià)太大不利于程序的優(yōu)化。但是有時(shí)候你需要對編譯器的優(yōu)化行為做出一定的約束,才能保證你的程序行為和你預(yù)期的執(zhí)行結(jié)果保持一致,那么這個(gè)約束就是內(nèi)存模型。
C++程序員想要寫出高性能的多線程程序必須理解內(nèi)存模型,編譯器會(huì)給你的程序做靜態(tài)優(yōu)化,CPU為了提升性能也有動(dòng)態(tài)亂序執(zhí)行的行為??傊?,實(shí)際編程中程序不會(huì)完全按照你原始代碼的順序來執(zhí)行,因此內(nèi)存模型就是程序員、編譯器、CPU之間的契約。編程、編譯、執(zhí)行都會(huì)在遵守這個(gè)契約的情況下進(jìn)行,在這樣的規(guī)則之上各自做自己的優(yōu)化,從而提升程序的性能。

內(nèi)存的順序描述了計(jì)算機(jī)CPU獲取內(nèi)存的順序,內(nèi)存的排序可能靜態(tài)也可能動(dòng)態(tài)的發(fā)生:
靜態(tài)內(nèi)存排序:編譯器期間,編譯器對內(nèi)存重排
動(dòng)態(tài)內(nèi)存排序:運(yùn)行期間,CPU亂序執(zhí)行
靜態(tài)內(nèi)存排序是為了提高代碼的利用率和性能,編譯器對代碼進(jìn)行了重新排序;同樣為了優(yōu)化性能CPU也會(huì)進(jìn)行對指令進(jìn)行重新排序、延緩執(zhí)行、各種緩存等等,以便達(dá)到更好的執(zhí)行效果。雖然經(jīng)過排序確實(shí)會(huì)導(dǎo)致很多執(zhí)行順序和源碼中不一致,但是你沒有必要為這些事情感到棘手足無措。任何的內(nèi)存排序都不會(huì)違背代碼本身所要表達(dá)的意義,并且在單線程的情況下通常不會(huì)有任何的問題。
但是在多線程場景中,無鎖的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)中,指令的亂序執(zhí)行會(huì)造成無法預(yù)測的行為。所以我們通常引入內(nèi)存柵欄這一概念來解決可能存在的并發(fā)問題。
Memory Barrier
內(nèi)存柵欄是一個(gè)令 CPU 或編譯器在內(nèi)存操作上限制內(nèi)存操作順序的指令,通常意味著在 barrier 之前的指令一定在 barrier 之后的指令之前執(zhí)行。
在 C11/C++11 中,引入了六種不同的 memory order,可以讓程序員在并發(fā)編程中根據(jù)自己需求盡可能降低同步的粒度,以獲得更好的程序性能。這六種 order 分別是:
relaxed,?acquire,?release,?consume,?acq_rel,?seq_cst
C++11中規(guī)定了如下6種訪問次序(Memory Oreder)
enum?memory_order?{
????memory_order_relaxed,
????memory_order_consume,
????memory_order_acquire,
????memory_order_release,
????memory_order_acq_rel,
????memory_order_seq_cst
};
上述6種訪問次序可以分為3類
順序一致性模型 std::memory_order_seq_cst
acquire-release 獲取/釋放語義模型std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel
Relax 寬松的內(nèi)存序列化模型:又稱為自由序列模型 ,(std::memory_order_relaxed)
以上三類可以組合成如下種類的模型:

在單個(gè)線程內(nèi),所有原子操作時(shí)順序進(jìn)行的。按照什么順序?按照代碼順序,這也就是該模型的唯一限制。兩個(gè)來自不同線程的原子操作順序是任意的
獲取釋放語義模型
Release -- acquire模型,在自由序列模型中來自兩個(gè)線程之間的原子操作時(shí)順序是不一定的,那么就需要把兩個(gè)線程進(jìn)行一下同步(synchronize-with)。同步什么?同步對一個(gè)變量的讀寫操作。線程A原子性的把值寫入X(release),然后線程B原子性地讀取X的值(acquire)。這樣線程B保證讀取到X的新值。需要注意的是release有個(gè)牛逼的副作用-線程A中所有發(fā)生在releaseX之前的寫操作,對線程BacqueireX之后的任何讀操作都可見!本來A,B之間的讀寫操作順序不定。這么已同步在X這個(gè)點(diǎn)前后,A,B線程之間有了個(gè)順序關(guān)系,稱作inter-thread happens-before
release-consume模型:不會(huì)吧,只是想要同步一個(gè)x的讀寫操作,結(jié)果把release之前的所有寫操作都順帶同步了?。。∵@對于不需要所有都同步的操作來說開銷也太大了。有沒有辦法能夠有效降低開銷,又能同步X呢?用release-consume唄,同步還是一樣的同步。這一會(huì)的副作用弱了點(diǎn):在線程B acqueire X之后的讀操作中,有一些是依賴X的值的讀操作。管這些依賴X的讀操作叫做-賴B讀。同理在線程A里面,release X也有一些它所依賴的其他寫操作,這些操作自然發(fā)生在release X之前了,管這些操作叫做-賴A寫。副作用總結(jié)來說就是,只有賴B讀能看見賴A寫
那么寫到這里了,什么叫做賴,賴就是依賴的意思,release-acquire模型是大把抓的形式,把所有release之前的寫和acquire之后的讀都進(jìn)行同步。而release-consume控制的更加精細(xì),只有有依賴關(guān)系的才進(jìn)行同步,依賴關(guān)系可以按照如下理解:
數(shù)據(jù)依賴:
S1.?c?=?a?+?b;
S2.?e?=?c?+?d;
數(shù)據(jù)S2是依賴S1的因?yàn)樵谟?jì)算S2的時(shí)候需要依賴S1中計(jì)算出來的c值。
順序一致性模型
Sequential consistency:前面的模型理解了,順序一致性也就好理解了,release-acquire就同步了一個(gè)x,順序一致就是對所有的變量的所有原子操作都進(jìn)行同步。那么這樣一來所有的原子操作就跟由一個(gè)線程順序執(zhí)行似的
6種訪問次序的說明
memory_order_relaxed
memory_order_relaxed: 只保證當(dāng)前操作的原子性,不考慮線程間的同步,其他線程可能讀到新值,也可能讀到舊值。也就是說,原子操作標(biāo)記 memory_order_relaxed不是同步操作;它們不會(huì)在并發(fā)內(nèi)存訪問之間強(qiáng)加順序。它們只保證原子性和修改順序的一致性。
Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation's atomicity is guaranteed(see Relaxed ordering below)
例如:
//?Thread?1:
r1?=?y.load(std::memory_order_relaxed);?//?A
x.store(r1,?std::memory_order_relaxed);?//?B
//?Thread?2:
r2?=?x.load(std::memory_order_relaxed);?//?C?
y.store(42,?std::memory_order_relaxed);?//?D
上述代碼會(huì)產(chǎn)生r1==r2, ==42的情況,盡管A在線程1中序列在B的前面,C的序列在線程2中在D的前面,沒有什么能夠阻止D排序到A的前面,也沒有什么能夠阻止B排到A的前面。唯一可以預(yù)見的是線程1中x的存儲(chǔ)A對線程2中的C可見。也就是使用memory_order_relaxed除了保證當(dāng)前原子變量的可見性和原子性。
即使是memory_order_relaxed也不能出現(xiàn)循環(huán)依賴的情況
//?Thread?1:
r1?=?y.load(std::memory_order_relaxed);
if?(r1?==?42)?x.store(r1,?std::memory_order_relaxed);
//?Thread?2:
r2?=?x.load(std::memory_order_relaxed);
if?(r2?==?42)?y.store(42,?std::memory_order_relaxed);
上述代碼不可能出現(xiàn)r1== r2 , ?== 42, 的情況,因?yàn)閞1== 42, 依賴r2存儲(chǔ)為42,而, r2 ==42依賴r1存儲(chǔ)為42。上述代碼直到C++14才被規(guī)則上允許,但是還是不建議使用上述方式給用戶實(shí)現(xiàn)since C++14
relaxed的通常使用方式是用來計(jì)數(shù),例如 std::shared_ptr中的引用計(jì)數(shù),因?yàn)檫@只需要原子性,并不需要進(jìn)行排序或者同步
?
std::atomic<int>?cnt?=?{0};
//?memory_order_relaxed保證當(dāng)前變量在不同線程中的原子性
void?f()
{
????for?(int?n?=?0;?n?<?1000;?++n)?{
????????cnt.fetch_add(1,?std::memory_order_relaxed);
????}
}
?
int?main()
{
????std::vector<std::thread>?v;
????for?(int?n?=?0;?n?<?10;?++n)?{
????????v.emplace_back(f);
????}
????for?(auto&?t?:?v)?{
????????t.join();
????}
????std::cout?<<?"Final?counter?value?is?"?<<?cnt?<<?'\n';
}
Output:
Final?counter?value?is?10000
memory_order_release
A store operation with this memory order performs the release operation: no reads or writes in the current thread can be reordered after this store. All writes in the current thread are visible in other threads that acquire the same atomic variable (see Release-Acquire ordering below) and writes that carry a dependency into the atomic variable become visible in other threads that consume the same atomic (see Release-Consume ordering below).
memory_order_release:(可以理解為 mutex 的 unlock 操作)
對寫入施加 release 語義(store),在代碼中這條語句前面的所有讀寫操作都無法被重排到這個(gè)操作之后,即 store-store 不能重排為 store-store, load-store 也無法重排為 store-load
當(dāng)前線程內(nèi)的所有寫操作,對于其他對這個(gè)原子變量進(jìn)行 acquire 的線程可見
當(dāng)前線程內(nèi)的與這塊內(nèi)存有關(guān)的所有寫操作,對于其他對這個(gè)原子變量進(jìn)行 consume 的線程可見
memory_order_acquire
memory_order_acquire: (可以理解為 mutex 的 lock 操作)
A load operation with this memory order performs the acquire operation on the affected memory location: no reads or writes in the current thread can be reordered before this load. All writes in other threads that release the same atomic variable are visible in the current thread (see Release-Acquire ordering below)
對讀取施加 acquire 語義(load),在代碼中這條語句前面所有讀寫操作都無法重排到這個(gè)操作之前,即 load-store 不能重排為 store-load, load-load 也無法重排為 load-load
這里一定要理解原子操作、內(nèi)存屏障的作用,在如下代碼你可以想象運(yùn)行期間絕對能保證 c.store(3, memory_order_release)與c.load(memory_order_acquire)是原子的,那么在acquire之前的不能進(jìn)行重新排,就保證了a,b的值是自己想要的,記住是跨線程的原子性哦。
在這個(gè)原子變量上施加 release 語義的操作發(fā)生之后,acquire 可以保證讀到所有在 release 前發(fā)生的寫入,舉個(gè)例子:
c?=?0;
thread?1:{?
????a?=?1;?
????b.store(2,?memory_order_relaxed);?
????c.store(3,?memory_order_release);
}
thread?2:{?
????while?(c.load(memory_order_acquire)?!=?3)?;?//?以下?assert?永遠(yuǎn)不會(huì)失敗?
????assert(a?==?1?&&?b?==?2);
?????assert(b.load(memory_order_relaxed)?==?2);
}
如果你個(gè)原子變量在線程A存儲(chǔ)時(shí)被打上了memory_order_release標(biāo)簽,在線程B加載的時(shí)候被打上了memory_order_acquire標(biāo)簽。A線程中所有在原子變量存儲(chǔ)之前的寫操作(包括非原子變量和relaxed標(biāo)記的原子變量)都會(huì)變得在B線程可見。也就是只要線程B中完成了load操作那么就可以保證在線程A中寫入的所有數(shù)據(jù)在線程B中可見。
同步的效果只有在對同一個(gè)原子變量releasing和acquiring的線程間有效果,對于其他的線程無效
Mutual exclusion locks, such as std::mutex or atomic spinlock, are an example of release-acquire synchronization: when the lock is released by thread A and acquired by thread B, everything that took place in the critical section (before the release) in the context of thread A has to be visible to thread B (after the acquire) which is executing the same critical section.
?std::atomic<std::string*>?ptr;
int?data;
?void?producer()
{
????std::string*?p??=?new?std::string("Hello");
????data?=?42;
????//?在調(diào)用release之后,其上的所有寫操作對于acquire的線程可見
????ptr.store(p,?std::memory_order_release);
}
?void?consumer()
{
????std::string*?p2;
????//?load?調(diào)用acquire之后,其下的所有可讀數(shù)據(jù)都在release之后
????while?(!(p2?=?ptr.load(std::memory_order_acquire)))
????????;
????assert(*p2?==?"Hello");?//?never?fires
????assert(data?==?42);?//?never?fires
}
?int?main()
{
????std::thread?t1(producer);
????std::thread?t2(consumer);
????t1.join();?t2.join();
}
如下代碼通過原子變量實(shí)現(xiàn)了三個(gè)線程之間的同步
std::vector<int>?data;
std::atomic<int>?flag?=?{0};
void?thread_1()
{
????data.push_back(42);
????flag.store(1,?std::memory_order_release);
}
void?thread_2()
{
????int?expected=1;
????//?執(zhí)行順序先acq?再rel
????while?(!flag.compare_exchange_strong(expected,?2,?std::memory_order_acq_rel))?{
????????expected?=?1;
????}
}
void?thread_3()
{
????std::this_thread::sleep_for(std::chrono::milliseconds?(10000));
????//?只有flag的值不小于2的時(shí)候才退出
????while?(flag.load(std::memory_order_acquire)?<?2)
????????;
????assert(data.at(0)?==?42);?//?will?never?fire
}
/*
?*?經(jīng)過上述操作執(zhí)行順序是,先線程1再線程2然后是線程3,通過原子變量實(shí)現(xiàn)了線程間的同步
?*?*/
int?main(int?argc,?char*argv[])
{
????std::thread?a(thread_1);
????std::thread?b(thread_2);
????std::thread?c(thread_3);
????a.join();?b.join();?c.join();
????return?0;
}
memory_order_consume
對當(dāng)前要讀取的內(nèi)存施加 release 語義(store),在代碼中這條語句后面所有與這塊內(nèi)存有關(guān)的讀寫操作都無法被重排到這個(gè)操作之前。也就是說當(dāng)一個(gè)原子變量在線程A中被標(biāo)記為memory_order_release
,同樣 原子變量線程B被使用memory_order_consume標(biāo)記進(jìn)行加載的時(shí)候,所有發(fā)生在A線程中原子變量store之前的寫操作,對于那些 依賴關(guān)系的元素都將在B線程中可見
std::atomic<std::string*>?ptr;
int?data;
void?producer()
{
????auto?*?p??=?new?std::string("Hello");
????data?=?42;
????ptr.store(p,?std::memory_order_release);
}
void?consumer()
{
????std::string*?p2;
????while?(!(p2?=?ptr.load(std::memory_order_consume)))
????????;
????//?因?yàn)閜2一定依賴原子變量ptr所有這里一定成功
????assert(*p2?==?"Hello");?//?never?fires:?*p2?carries?dependency?from?ptr
????assert(data?==?42);?//?may?or?may?not?fire:?data?does?not?carry?dependency?from?ptr
}
int?main()
{
????std::thread?t1(producer);
????std::thread?t2(consumer);
????t1.join();?t2.join();
}
The synchronization is established only between the threads releasing and consuming the same atomic variable. Other threads can see different order of memory accesses than either or both of the synchronized threads.
使用場景
Typical use cases for this ordering involve read access to rarely written concurrent data structures (routing tables, configuration, security policies, firewall rules, etc) and publisher-subscriber situations with pointer-mediated publication, that is, when the producer publishes a pointer through which the consumer can access information: there is no need to make everything else the producer wrote to memory visible to the consumer (which may be an expensive operation on weakly-ordered architectures). An example of such scenario is rcu_dereference.
A load operation with this memory order performs a consume operation on the affected memory location: no reads or writes in the current thread dependent on the value currently loaded can be reordered before this load. Writes to data-dependent variables in other threads that release the same atomic variable are visible in the current thread. On most platforms, this affects compiler optimizations only (see Release-Consume ordering below)
在這個(gè)原子變量上施加 release 語義的操作發(fā)生之后,acquire 可以保證讀到所有在 release 前發(fā)生的并且與這塊內(nèi)存有關(guān)的寫入,舉個(gè)例子:
a?=?0;
c?=?0;
thread?1:{
????a?=?1;?
????c.store(3,?memory_order_release);
}
thread?2:{?
????while?(c.load(memory_order_consume)?!=?3)?;?
????assert(a?==?1);?//?assert?可能失敗也可能不失敗
}
memory_order_acq_rel
A read-modify-write operation with this memory order is both an acquire operation and a release operation. No memory reads or writes in the current thread can be reordered before or after this store. All writes in other threads that release the same atomic variable are visible before the modification and the modification is visible in other threads that acquire the same atomic variable.
對讀取和寫入施加 acquire-release 語義,無法被重排
可以看見其他線程施加 release 語義的所有寫入,同時(shí)自己的 release 結(jié)束后所有寫入對其他施加 acquire 語義的線程可見
memory_order_seq_cst(順序一致性)
A load operation with this memory order performs an acquire operation, a store performs a release operation, and read-modify-write performs both an acquire operation and a release operation, plus a single total order exists in which all threads observe all modifications in the same order (see Sequentially-consistent ordering below)
如果是讀取就是 acquire 語義,如果是寫入就是 release 語義,如果是讀取+寫入就是 acquire-release 語義
同時(shí)會(huì)對所有使用此 memory order 的原子操作進(jìn)行同步,所有線程看到的內(nèi)存操作的順序都是一樣的,就像單個(gè)線程在執(zhí)行所有線程的指令一樣
通常情況下,默認(rèn)使用 memory_order_seq_cst,所以你如果不確定怎么這些 memory order,就用這個(gè)。
擴(kuò)展
評論里有很多關(guān)于x86內(nèi)存模型的指正,放在這里:
Loads are not reordered with other loads.Stores are not reordered with other stores.Stores are not reordered with older loads.
然后最重要的:
Loads may be reordered with older stores to different locations.
因?yàn)?store-load 可以被重排,所以x86不是順序一致。但是因?yàn)槠渌N讀寫順序不能被重排,所以x86是 acquire/release 語義。
aquire語義:load 之后的讀寫操作無法被重排至 load 之前。即 load-load, load-store 不能被重排。
release語義:store 之前的讀寫操作無法被重排至 store 之后。即 load-store, store-store 不能被重排。
參考: 詳細(xì)的信息一直在飛書文檔上進(jìn)行實(shí)時(shí)更新:C++內(nèi)存一致性和原子操作https://ny5odfilnr.feishu.cn/docs/doccnnvMCH7nPKidScKSnJmiJie