无论是纯粹基于原子操作的同步,还是利用fence的,最常用的两种就是release acquire和Sequentially-consistent ordering,基于原子操作的上篇博客已经讲过了,接下来就重点聊一下引入fence的相关同步。需要注意的一点是,c++的memory_order属于逻辑上的内存变量的可见性和顺序的限制,并不等同于底层的memory barrier,但在实际的底层实现上就是选择合适的具有特定memory barrier效果的cpu指令,换句话说,指定memory_order可以得到特定的memory barrier效果。
和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的效果。
Release fence可以防止fence前的内存操作重排到fence后的任意store之后,即阻止loadstore重排和storestore重排。
acquire fence可以防止fence后的内存操作重排到fence前的任意load之前,即阻止loadload重排和loadstore重排
因为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指令,这个后面的文章再详细说明。
基于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);
因为fence的同步效果和原子操作上的同步效果比较相似,可以互相组合,自然的,使用fence的同步会有三种情况,
fence - atomic同步,fence - fence同步和atomic-fence -同步。下面是三种同步中关于release 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 。
线程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 。
有线程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。
以上形式化定义看着很繁琐,但理解了就会觉得很自然,和原来纯基于原子变量的很类似,除了效果更强可以作用于多个原子变量意外。
除了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?
下面贴两个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()
}
}
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