C++ memory order循序渐进(四)—— 在std::atomic_thread_fence 上应用std::memory_order实现不同的内存序

文章目录

  • 1. atomic_thread_fence分类和效果
    • 1.1 Release fence
    • 1.2 acquire fence
    • 1.3 full fence
  • 2. fence和同样memory order的原子操作同步效果的区别
  • 3. 利用atomic_thread_fence进行release acquire同步
    • 3.1 release fence - atomic acquire 同步
    • 3.2 atomic release- acquire fence 同步
    • 3.3 release fence - acquire fence 同步
  • 4. 利用atomic_thread_fence进行Sequentially-consistent 同步
  • 5. fence同步实例
  • 6. 总结

上一篇博客介绍了如何在原子变量的存取上应用不同的memory order来实现不同的内存序来达到数据同步的目的,而在C++ 11及之后的标准里,除了利用原子操作指定内存序,还定义了单独使用memory fence(std::atomic_thread_fence)的方式,fence可以和原子操作组合进行同步,也可以fence之间进行同步,fence不光可以不依赖原子操作进行同步,而且相比较于同样memory order的原子操作,具有更强的内存同步效果,下面就结合实例来介绍下c++里和fence相关的同步手段。

无论是纯粹基于原子操作的同步,还是利用fence的,最常用的两种就是release acquire和Sequentially-consistent ordering,基于原子操作的上篇博客已经讲过了,接下来就重点聊一下引入fence的相关同步。需要注意的一点是,c++的memory_order属于逻辑上的内存变量的可见性和顺序的限制,并不等同于底层的memory barrier,但在实际的底层实现上就是选择合适的具有特定memory barrier效果的cpu指令,换句话说,指定memory_order可以得到特定的memory barrier效果。

1. atomic_thread_fence分类和效果

和atomic变量类似,atomic_thread_fence也可以指定六种memory order,指定不同memory order的fence可以分为以下几类:
(1) std::atomic_thread_fence(memory_order_relaxed),没有任何效果。
(2) std::atomic_thread_fence(memory_order_acquire) 和 std::atomic_thread_fence(memory_order_consume) 属于acquire fence。
(3)std::atomic_thread_fence(memory_order_release)属于release fence。
(4)std::atomic_thread_fence(memory_order_acq_rel)既是acquire fence 也是release fence,为了方便这里称为full fence。
(5)std::atomic_thread_fence(memory_order_seq_cst)额外保证有单独全序的full fence。

也就是说,如果不考虑单独全序,那么有release fence、acquire fence 和full fence三种。下面就根据以前介绍过的四种重排来介绍下这三种fence的效果。

1.1 Release fence

Release fence可以防止fence前的内存操作重排到fence后的任意store之后,即阻止loadstore重排和storestore重排。
C++ memory order循序渐进(四)—— 在std::atomic_thread_fence 上应用std::memory_order实现不同的内存序_第1张图片

1.2 acquire fence

acquire fence可以防止fence后的内存操作重排到fence前的任意load之前,即阻止loadload重排和loadstore重排
C++ memory order循序渐进(四)—— 在std::atomic_thread_fence 上应用std::memory_order实现不同的内存序_第2张图片

1.3 full fence

因为full fence是release fence和acquire fence的组合,所以也就是防止loadload、loadstore、storestore重排,std::atomic_thread_fence(memory_order_acq_rel)和std::atomic_thread_fence(memory_order_seq_cst)都是full fence。注意,在c++的标准定义里,full fence并没有规定一定要阻止storeload重排,即便是std::atomic_thread_fence(memory_order_seq_cst)也一样,只是需要额外保证单独全序,但是在实际的实现上为了实现这个全序编译器大都是采用了硬件层面的能够阻止storeload重排的full barrier指令,这个后面的文章再详细说明。
C++ memory order循序渐进(四)—— 在std::atomic_thread_fence 上应用std::memory_order实现不同的内存序_第3张图片

2. fence和同样memory order的原子操作同步效果的区别

基于atomic_thread_fence(外加一个任意序的原子变量操作)的同步和基于原子操作的同步很类似,比如最常用的,都可以形成release acquire语义,但是从上面的描述可以看出,fence的效果要比基于原子变量的效果更强,在weak memory order平台的开销也更大。

以release为例,对于基于原子变量的release opration,仅仅是阻止前面的内存操作重排到该release opration之后,而release fence则是阻止重排到fence之后的任意store operation之后,比如一个简单的例子:

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

以下代码具有同样效果:

std::string* p  = new std::string("Hello");
std::atomic_thread_fence(memory_order_release);
ptr.store(p, std::memory_order_relaxed);

但是再比如:
(1)依赖ptr1的线程永远能读到正确值,但是依赖ptr2的不一定。

std::string* p  = new std::string("Hello");
ptr1.store(p, std::memory_order_release);
ptr2.store(p, std::memory_order_relaxed);

(2)依赖ptr1和ptr2的的线程都永远能读到正确值

std::string* p  = new std::string("Hello");
std::atomic_thread_fence(memory_order_release);
ptr1.store(p, std::memory_order_relaxed);
ptr2.store(p, std::memory_order_relaxed);

3. 利用atomic_thread_fence进行release acquire同步

因为fence的同步效果和原子操作上的同步效果比较相似,可以互相组合,自然的,使用fence的同步会有三种情况,
fence - atomic同步,fence - fence同步和atomic-fence -同步。下面是三种同步中关于release acquire的形式化定义。

3.1 release fence - atomic acquire 同步

线程A有一个 release fence FA,线程B有一个acquire operation Y ,如果满足以下三个条件,那么会形成release acquire语义进而形成synchronizes-with 关系,从而线程A的FA之前的所有写入都会happen-before Y之后的读,也就是对Y可见:
1.有一个任意memory order的 atomic store X 。
2.Y读到了X写入的值 (或者如果x是release operation ,读到了release sequence headed by X 写入的值)。
3.FA sequenced-before X 。

3.2 atomic release- acquire fence 同步

线程A有一个release operation X,线程B中有一个acquire fence FB,如果满足以下三个条件,那么会形成release acquire语义进而形成synchronizes-with 关系,从而 thread A 中所有sequenced-before X的内存写入都会happen-before 线程B中FB后的读:
1.B中有一个任意memory order的 atomic read Y。
2.Y 读到了 X或者release sequence headed by X写入的值 。
3.Y sequenced-before FB 。

3.3 release fence - acquire fence 同步

有线程A中的 release fence FA,和线程B中的 acquire fence FB, 如果满足以下条件,那么会形成release acquire语义进而形成synchronizes-with 关系,从而线程A中所有 sequenced-before FA的写入都会 happen-before 线程B中FB之后的所有读取:
1.有一个原子变量 M。
2.线程A中有一个任意memory order 的对M的原子写入X。
3.FA sequenced-before X。
4.线程B中有一个任意memory order 的对M的原子读Y读到了 X 写入的值 (或者如果x是release operation ,读到了release sequence headed by X 写入的值)。
5.Y sequenced-before FB。

以上形式化定义看着很繁琐,但理解了就会觉得很自然,和原来纯基于原子变量的很类似,除了效果更强可以作用于多个原子变量意外。

4. 利用atomic_thread_fence进行Sequentially-consistent 同步

除了release acquire,另一个比较常用的内存序就是memory_order_seq_cst了,前面说到了,memory_order_seq_cst需要保证有一个单独全序(各线程观察到的修改顺序一致)从而保证顺序一致,注意这里说的单独全序仅仅是memory_order_seq_cst operation之间的,如果插入了低等级的就无法“单独”了。fence有以下顺序一致的保证:
(1)设有一个load操作B 从原子对象M读,如果有一个sequenced-before B的memory_order_seq_cst fence X,那么B会观察到以下二者之一:
1.全序中位于X前并离X最近的对M的memory_order_seq_cst 修改
2.随后的M modification order上的一些无关修改

(2)对于M上的一对原子操作store A和load B,如果额外有一个memory_order_seq_cst fence FX, 且A sequenced-before FX,在全序上FX早于B,那么B观察到以下二者之一:
1.A写入的值
2.M的modification order上A之后的无关修改

(3)对于M上的一对原子操作store A和load B,如果额外有两个memory_order_seq_cst fence FX和FY, 有A sequenced-before FX, FY sequenced-before B,并且在全序上FX早于FY, 那么B观察到以下2者之一:
1.A写入的值
2.M的modification order 上A之后的无关修改

(4)对于M上的一对修改操作store A和store B, 如果满足以下条件之一,那么在M的modification order 上B在A之后:
1.有一个memory_order_seq_cst fence FX,有A sequenced-before FX ,全序里FX 在B之前。
2.有一个memory_order_seq_cst fence FY ,有FY sequenced-before B,全序里A 在FY之前。
3.有两个memory_order_seq_cst fence FX和FY ,有A sequenced-before FX,FY sequenced-before B,全序里FX 在FY之前。

这些规则重点突出了非memory_order_seq_cst操作的影响,有些晦涩难懂,stackoverflow上面有几个不错的相关问题可以加深理解:
1.How to achieve a StoreLoad barrier in C++11?
2.Does atomic_thread_fence(memory_order_seq_cst) have the semantics of a full memory barrier?
3.Does standard C++11 guarantee that memory_order_seq_cst prevents StoreLoad reordering of non-atomic around an atomic?

5. fence同步实例

下面贴两个cppreference上的使用fence同步的实例:
(1)fence-fence同步

//Global
std::string computation(int);
void print( std::string );
 
std::atomic<int> arr[3] = { -1, -1, -1 };
std::string data[1000] //non-atomic data
 
// Thread A, compute 3 values
void ThreadA( int v0, int v1, int v2 )
{
//assert( 0 <= v0, v1, v2 < 1000 );
data[v0] = computation(v0);
data[v1] = computation(v1);
data[v2] = computation(v2);
std::atomic_thread_fence(std::memory_order_release);
std::atomic_store_explicit(&arr[0], v0, std::memory_order_relaxed);
std::atomic_store_explicit(&arr[1], v1, std::memory_order_relaxed);
std::atomic_store_explicit(&arr[2], v2, std::memory_order_relaxed);
}
 
// Thread B, prints between 0 and 3 values already computed.
void ThreadB()
{
int v0 = std::atomic_load_explicit(&arr[0], std::memory_order_relaxed);
int v1 = std::atomic_load_explicit(&arr[1], std::memory_order_relaxed);
int v2 = std::atomic_load_explicit(&arr[2], std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
// v0, v1, v2 might turn out to be -1, some or all of them.
// otherwise it is safe to read the non-atomic data because of the fences:
if( v0 != -1 ) { print( data[v0] ); }
if( v1 != -1 ) { print( data[v1] ); }
if( v2 != -1 ) { print( data[v2] ); }
}

(2)atomic fence同步

const int num_mailboxes = 32;
std::atomic<int> mailbox_receiver[num_mailboxes];
std::string mailbox_data[num_mailboxes];
 
// The writer threads update non-atomic shared data 
// and then update mailbox_receiver[i] as follows
mailbox_data[i] = ...;
std::atomic_store_explicit(&mailbox_receiver[i], receiver_id, std::memory_order_release);
 
// Reader thread needs to check all mailbox[i], but only needs to sync with one
for (int i = 0; i < num_mailboxes; ++i) {
    if (std::atomic_load_explicit(&mailbox_receiver[i], std::memory_order_relaxed) == my_id) {
        std::atomic_thread_fence(std::memory_order_acquire); // synchronize with just one writer
        do_work( mailbox_data[i] ); // guaranteed to observe everything done in the writer thread before
                    // the atomic_store_explicit()
    }
 }

6. 总结

c++ memory order里,除了原子操作,还有独立的fence可以用来指定内存序,后者具有更强的同步效果,可以根据实际情况按需使用。前面说过,c++ 的memory order是给程序员提供的一种控制可见性和内存序的手段,即特定的编码方式能达到特定的内存同步效果,而要达到memory order 承诺的效果,则需要编译器乃至cpu进行相应的一些操作,后面将会介绍c++ memory order具体是如何在不同的硬件平台上实现的,虽然设计上底层细节对编码者是透明的,但理解了底层实现更有利于我们正确地使用c++ memory order。

参考:
https://en.cppreference.com/w/cpp/atomic/memory_order
https://preshing.com
http://www.modernescpp.com/index.php/fences-as-memory-barriers
https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence
https://stackoverflow.com/questions/25478029/does-atomic-thread-fencememory-order-seq-cst-have-the-semantics-of-a-full-memo

你可能感兴趣的:(c++,memory_order,c++,后端,并发编程,c++11,多线程)