在原子变量的存取上应用不同的memory order可以实现不同的内存序来达到数据同步的目的,而在C++ 11及之后的标准里,除了利用原子操作指定内存序,还定义了单独使用“内存栅栏”(std::atomic_thread_fence)的方式,fence可以和原子操作组合进行同步,也可以fence之间进行同步,fence不光可以不依赖原子操作进行同步,而且相比较于同样memory order的原子操作,具有更强的内存同步效果,下面就结合实例来介绍下c++里和fence相关的同步手段。
无论是纯粹基于原子操作的同步,还是利用fence的,最常用的两种就是release acquire和Sequentially-consistent ordering,引入fence的相关同步:
1. atomic_thread_fence分类和效果
和atomic变量类似,atomic_thread_fence也可以指定六种memory order,指定不同memory order的fence可以分为以下几类:
std::atomic_thread_fence(memory_order_relaxed) //没有任何效果。
std::atomic_thread_fence(memory_order_acquire) 与 std::atomic_thread_fence(memory_order_consume) // 属于acquire fence
std::atomic_thread_fence(memory_order_release) // 属于release fence
std::atomic_thread_fence(memory_order_acq_rel) // 既是acquire fence 也是release fence,为了方便这里称为full fence
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重排。
1.2 acquire fence
acquire fence可以防止fence后的内存操作重排到fence前的任意load之前,即阻止loadload重排和loadstore重排
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指令,这个后面的文章再详细说明。
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之后的读:
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需要保证有一个单独全序从而保证顺序一致,fence有以下顺序一致的保证。
(1)设有一个read operation B ,和一个sequenced-before B的memory_order_seq_cst fence X,那么B会观察到以下两种情况之一:
1.单独全序中X前并离X最近的对M的memory_order_seq_cst 修改
2.M上随后的M modification order上的一些无关修改
(2)对于M上的一对原子操作store A和memory_order_seq_cst load B,如果额外有一个memory_order_seq_cst fence FX, 且A sequenced-before FX,在单独全序上FX早于B, 那么B观察到以下2者之一:
1.A写入的值
2.M的modification order上A之后的无关修改
(3)对于M上的一对写A和读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 ,B是memory_order_seq_cst 且在实际运行时的单独全序上FX 在B之前。
2.有一个memory_order_seq_cst fence FY ,有FY sequenced-before B,A是memory_order_seq_cst 且在实际运行时的单独全序上A 在FY之前。
3.有两个memory_order_seq_cst fence FX和FY ,有A sequenced-before FX,FY sequenced-before B,在实际运行时的单独全序上FX 在FY之前。
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。