GNU C/C++中的内存模型与原子操作

一、历史版本__sync

__sync原子操作函数集合,是GCC根据intel官方手册搞出的原子操作集合。

type __sync_fetch_and_OP (type *ptr, type value, ...)
type __sync_OP_and_fetch (type *ptr, type value, ...)
bool__sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
__sync_synchronize (...)

type __sync_lock_test_and_set (type *ptr, type value, ...)
void__sync_lock_release (type *ptr, ...)

上面的OP操作包括add、sub、or、and、xor、nand这些常见的数学操作,而type表示的数据类型Intel官方允许的是int、long、long long的带符号和无符号类型,但是GCC扩展后允许任意1/2/4/8的标量类型;CAS的操作有两个版本分别返回bool表示是否成功,而另外一个在操作之前会先返回ptr地址处存储的值;__sync_synchronize直接插入一个full memory barrier,当然你也可能经常见到像asm volatile(“” ::: “memory”);这样的操作。前面的这些原子操作都是full barrier类型的,这意味着:任何内存操作的指令不允许跨越这些操作重新排序。

__sync_lock_test_and_set用于将value的值写入ptr的位置,同时返回ptr之前存储的值,其内存模型是acquire barrier,意味着该操作之后的memory store指令不允许重排到该操作之前去,不过该操作之前的memory store可以排到该操作之后去,而__sync_lock_release则更像是对前面一个操作锁的释放,通常意味着将0写入ptr的位置,该操作是release barrier,意味着之前的memory store是全局可见的,所有的memory load也都完成了,但是接下来的内存读取可能会被排序到该操作之前执行。可以这里比较绕,翻译起来也比较的拗口,不过据我所见,这里很多是用在自旋锁类似的操作上,比如:

staticvolatileint_sync;

staticvoidlock_sync(){
while(__sync_lock_test_and_set(&_sync,1));
}
staticvoidunlock_sync(){
__sync_lock_release(&_sync);

二、C++11 新标准中的内存模型

原子变量的通用接口使用store()和load()方式进行存取,可以额外接受一个额外的memory order参数,而不传递的话默认是最强模式Sequentially Consistent。

根据执行线程之间对变量的同步需求强度,新标准下的内存模型可以分成如下几类:

(一些个人理解,详细的请看原文)

Sequentially Consistent

这是最严格一致性协议,非常好理解。无论你有多少个线程,每次切换到新的线程,你的缓存中都会先从内存中load最新的值,每次线程结束,要切换到其他线程去执行时,都会把缓存中的内容刷到内存中去。这样就保证了无论执行任何一个线程到任何一步,再切换到其他线程执行时,新线程所看到的都是最新的值(即都是happend before的)。在sequentially consistent之前的指令可以进行指令重排。

Acquire/Release

这个相比于前一个就稍微宽松一些。对于Acquire来说,只有读是happened before的,对于Release来说,写是happened before的。详细来说就是,对于使用acquire的地方,后来的内存访问(读和写)一定要在这次load操作之后进行,之后的内存访问可以重排在load之前。 对于使用Release的地方,之前的内存访问都要在这次store之前完成,之后的内存访问可以在重排在store之前。二者组合使用。

Consume

这个也很好理解,它是和Acquire相对的,Acquire一定会把无关的也同步,但是Consume不不要同步无关的。

在GNU的官网有一句话总结的很好,就是

The real difference boils down to how much state the hardware has to flush in order to synchronize。

这些模型之间真正的不同说到底就是为了同步的目的,要刷到内存中多少的状态的不同。

可以看出这里synchronize的意思就是把缓存的值刷到内存。

三、C++11 GCC __atomic

GCC实现了C++11之后,上面的__sync系列操作就变成了Legacy而不被推荐使用了,而基于C++11的新原子操作接口使用__atomic作为前缀。

对于普通的数学操作函数,其函数接口形式为:

type __atomic_OP_fetch (type *ptr, type val,intmemorder);
type __atomic_fetch_OP (type *ptr, type val,intmemorder);

除此之外,还根据新标准提供了一些新的接口:

type __atomic_load_n (type *ptr,intmemorder);
void__atomic_store_n (type *ptr, type val,intmemorder);
type __atomic_exchange_n (type *ptr, type val,intmemorder);
bool__atomic_compare_exchange_n (type *ptr, type *expected, type desired,boolweak,intsuccess_memorder,intfailure_memorder);

bool__atomic_test_and_set (void*ptr,intmemorder);
void__atomic_clear (bool*ptr,intmemorder);

void__atomic_thread_fence (intmemorder);

bool__atomic_always_lock_free (size_tsize,void*ptr);
bool__atomic_is_lock_free (size_tsize,void*ptr);

从函数名,看起来意思也很明了吧,上面的带_n的后缀版本如果去掉_n就是不用提供memorder的seq_cst版本。最后的两个函数,是判断系统上对于某个长度的对象是否会产生lock-free的原子操作,一般long long这种8个字节是没有问题的,对于支持128位整形的构架就可以达到16字节无锁结构了。

本文转载自:https://www.cnblogs.com/the-tops/p/6347584.html?utm_source=itdadao&utm_medium=referral