詳解c++ atomic原子編程中的Memory Order
概述
但是,基于內(nèi)核對(duì)象的同步,會(huì)帶來(lái)昂貴的上下文切換(用戶(hù)態(tài)切換到內(nèi)核態(tài),占用1000個(gè)以上的cpu周期)。就需要使用另一種方法 —— 原子指令。

僅靠原子技術(shù)實(shí)現(xiàn)不了對(duì)資源的訪(fǎng)問(wèn)控制,即使簡(jiǎn)單計(jì)數(shù)操作,看上去正確的代碼也可能會(huì)crash。
這里的關(guān)鍵在于編譯器和cpu實(shí)施的重排指令導(dǎo)致了讀寫(xiě)順序的變化。只要沒(méi)有依賴(lài),代碼中在后面的指令就可能跑到前面去,編譯器和CPU都會(huì)這么做。
注1:?jiǎn)尉€(xiàn)程代碼不需要關(guān)心亂序的問(wèn)題。因?yàn)閬y序至少要保證這一原則:不能改變單線(xiàn)程程序的執(zhí)行行為
注2:內(nèi)核對(duì)象多線(xiàn)程編程在設(shè)計(jì)的時(shí)候都阻止了它們調(diào)用點(diǎn)中的亂序(已經(jīng)隱式包含memory barrier),不需要考慮亂序的問(wèn)題。
注3:使用用戶(hù)模式下的線(xiàn)程同步時(shí),亂序的效果才會(huì)顯露無(wú)疑。
程序員可以使用c++11 atomic提供了6種memory order,來(lái)在編程語(yǔ)言層面對(duì)編譯器和cpu實(shí)施的重排指令行為進(jìn)行控制

多線(xiàn)程編程時(shí),通過(guò)這些標(biāo)志位,來(lái)讀寫(xiě)原子變量,可以組合出4種同步模型:
Relaxed ordering
Release-Acquire ordering
Release-Consume ordering
Sequentially-consistent ordering
默認(rèn)情況下,std::atomic使用的是Sequentially-consistent ordering(最嚴(yán)格的同步模型)。但在某些場(chǎng)景下,合理使用其它3種ordering,可以讓編譯器優(yōu)化生成的代碼,從而提高性能。
Relaxed ordering
在這種模型下,std::atomic的load()和store()都要帶上memory_order_relaxed參數(shù)。Relaxed ordering僅僅保證load()和store()是原子操作,除此之外,不提供任何跨線(xiàn)程的同步。
先看看一個(gè)簡(jiǎn)單的例子:
std::atomic<int> x = 0; // global variable std::atomic<int> y = 0; // global variable Thread-1: Thread-2: r1 = y.load(memory_order_relaxed); // A r2 = x.load(memory_order_relaxed); // C x.store(r1, memory_order_relaxed); // B y.store(42, memory_order_relaxed); // D
執(zhí)行完上面的程序,可能出現(xiàn)r1 == r2 == 42。理解這一點(diǎn)并不難,因?yàn)榫幾g器允許調(diào)整 C 和 D 的執(zhí)行順序。
如果程序的執(zhí)行順序是 D -> A -> B -> C,那么就會(huì)出現(xiàn)r1 == r2 == 42。
如果某個(gè)操作只要求是原子操作,不需要其它同步的保障,就可以使用 Relaxed ordering。程序計(jì)數(shù)器是一種典型的應(yīng)用場(chǎng)景。
#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
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();
}
assert(cnt == 10000); // never failed
return 0;
}
Release-Acquire ordering
在這種模型下,store()使用memory_order_release,而load()使用memory_order_acquire。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
(1)在store()之前的所有讀寫(xiě)操作,不允許被移動(dòng)到這個(gè)store()的后面。 // write-release語(yǔ)義
(2)在load()之后的所有讀寫(xiě)操作,不允許被移動(dòng)到這個(gè)load()的前面。 // read-acquire語(yǔ)義
該模型可以保證:如果Thread-1的store()的那個(gè)值,成功被 Thread-2的load()到了,那么 Thread-1在store()之前對(duì)內(nèi)存的所有寫(xiě)入操作,此時(shí)對(duì) Thread-2 來(lái)說(shuō),都是可見(jiàn)的。
下面的例子闡述了這種模型的原理:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<bool> ready{ false };
int data = 0;
void producer()
{
data = 100; // A
ready.store(true, std::memory_order_release); // B
}
void consumer()
{
while (!ready.load(std::memory_order_acquire)) // C
;
assert(data == 100); // never failed // D
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
讓我們分析一下這個(gè)過(guò)程:
首先 A 不允許被移動(dòng)到 B 的后面。
同樣 D 也不允許被移動(dòng)到 C 的前面。
當(dāng) C 從 while 循環(huán)中退出了,說(shuō)明 C 讀取到了 B store()的那個(gè)值,此時(shí),Thread-2 保證能夠看見(jiàn) Thread-1 執(zhí)行 B 之前的所有寫(xiě)入操作(也即是 A)。
使用Release-Acquire ordering實(shí)現(xiàn)雙重檢查鎖模式(DLCP)
下面單件為例來(lái)說(shuō)明:
class Singleton
{
public:
static Singleton* get_instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::unique_lock<std::mutex> lk(mutex_);
tmp = instance_;
if (tmp == nullptr) {
tmp = new Singleton();
instance_.store(std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
使用Release-Acquire ordering實(shí)現(xiàn)自旋鎖(Spinlock)
獲取和釋放語(yǔ)義,是實(shí)現(xiàn)鎖的基礎(chǔ)(Spinlock, Mutex, RWLock, ...),所有被[Read Acquire,Write Release]包含的區(qū)域,即構(gòu)成了一個(gè)臨界區(qū),臨界區(qū)里的內(nèi)存操作,不會(huì)亂序到臨界區(qū)之外執(zhí)行。
read-acquire(判斷是否加鎖,沒(méi)則加鎖,否則循環(huán)等待)
-------------------------------------------------------------------------
all memory operation stay between the line(臨界區(qū))
-------------------------------------------------------------------------
write-release(釋放鎖)
實(shí)現(xiàn)代碼如下:
#include <atomic>
class simple_spin_lock
{
public:
simple_spin_lock() = default;
void lock()
{
while (flag.test_and_set(std::memory_order_acquire))
continue;
}
void unlock()
{
flag.clear(std::memory_order_release);
}
private:
simple_spin_lock(const simple_spin_lock&) = delete;
simple_spin_lock& operator =(const simple_spin_lock&) = delete;
std::atomic_flag flag = ATOMIC_FLAG_INIT;
};
①對(duì)std::atomic_flag的操作具有原子性,保證了同一時(shí)間,只有一個(gè)線(xiàn)程能夠lock成功,其余線(xiàn)程全部在while循環(huán)
②使用了acquire內(nèi)存屏障, 所以lock具有獲取語(yǔ)義
③使用了release內(nèi)存屏障, 所以u(píng)nlock具有釋放語(yǔ)義
Release-Consume ordering
在這種模型下,store()使用memory_order_release,而load()使用memory_order_consume。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
(1)在store()之前的所有讀寫(xiě)操作,不允許被移動(dòng)到這個(gè)store()的后面。
(2)在load()之后的所有依賴(lài)此原子變量的讀寫(xiě)操作,不允許被移動(dòng)到這個(gè)load()的前面。
注:不依賴(lài)此原子變量的讀寫(xiě)操作可能會(huì)CPU指令重排
下面的例子闡述了這種模型的原理:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
// thread1
void producer()
{
std::string* p = new std::string("Hello"); // A
data = 42; // B
ptr.store(p, std::memory_order_release); // C
}
// thread2
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume))) // D
;
assert(*p2 == "Hello"); //E always true: *p2 carries dependency from ptr
assert(data == 42); // F may be false: data does not carry dependency from ptr
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
Sequentially-consistent ordering
所有以memory_order_seq_cst為參數(shù)的原子操作(不限于同一個(gè)原子變量),對(duì)所有線(xiàn)程來(lái)說(shuō)有一個(gè)全局順序(total order)
并且兩個(gè)相鄰memory_order_seq_cst原子操作之間的其他操作(包括非原子變量操作),不能reorder到這兩個(gè)相鄰操作之外
UE4下的Memory Order
enum class EMemoryOrder
{
// Provides no guarantees that the operation will be ordered relative to any other operation.
Relaxed,
// Establishes a single total order of all other atomic operations marked with this.
SequentiallyConsistent // Load和Store函數(shù)缺省為該類(lèi)型
};
詳見(jiàn):UnrealEngine\Engine\Source\Runtime\Core\Public\Templates\Atomic.h
Atomic相關(guān)的測(cè)試代碼見(jiàn):UnrealEngine\Engine\Source\Runtime\Core\Private\Tests\Misc\AtomicTest.cpp

以上就是詳解c++ atomic原子編程中的Memory Order的詳細(xì)內(nèi)容,更多關(guān)于c++ atomic原子編程中的Memory Order的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++實(shí)踐IP地址類(lèi)項(xiàng)目參考
今天小編就為大家分享一篇關(guān)于C++實(shí)踐IP地址類(lèi)項(xiàng)目參考,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-02-02
c語(yǔ)言實(shí)現(xiàn)兩個(gè)單鏈表的交叉合并方式
今天小編就為大家分享一篇c語(yǔ)言實(shí)現(xiàn)兩個(gè)單鏈表的交叉合并方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-12-12
C++實(shí)現(xiàn)拷貝構(gòu)造函數(shù)的方法詳解
拷貝構(gòu)造函數(shù)是構(gòu)造函數(shù)的一個(gè)重載,因此顯式的定義了拷貝構(gòu)造,那么編譯器也不再默認(rèn)生成構(gòu)造函數(shù)。本文主要介紹了C++實(shí)現(xiàn)拷貝構(gòu)造函數(shù)的方法,需要的可以參考一下2022-09-09
VS2022新建項(xiàng)目時(shí)沒(méi)有ASP.NET Web應(yīng)用程序(.NET Framework)
本文主要介紹了VS2022新建項(xiàng)目時(shí)沒(méi)有ASP.NET Web應(yīng)用程序的解決,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-10-10
visual?studio?2022?編譯出來(lái)的文件被刪除并監(jiān)視目錄中的文件變更(示例詳解)
這篇文章主要介紹了visual?studio?2022?編譯出來(lái)的文件被刪除?并監(jiān)視目錄中的文件變更,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08
Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Matlab有一定幫助,需要的可以參考一下2022-09-09
詳解如何將Spire.XLS for C++集成到C++程序中
Spire.XLS for C++ 是一個(gè) Excel 庫(kù),供開(kāi)發(fā)人員在任何類(lèi)型的 C++ 應(yīng)用程序中操作 Excel 文檔(XLS、XLSX、XLSB 和 XLSM)。 本文演示了如何以?xún)煞N不同的方式將 Spire.XLS for C++ 集成到您的 C++ 應(yīng)用程序中2023-03-03
基于c中使用ftruncate()前需要fflush(),使用后需要rewind()的深入探討
本篇文章是對(duì)在c中使用ftruncate()前需要fflush(),使用后需要rewind()進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
C語(yǔ)言實(shí)現(xiàn)投票系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)投票系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07

