C++11开发中的Atomic原子操作 您所在的位置:网站首页 ATOMRC C++11开发中的Atomic原子操作

C++11开发中的Atomic原子操作

2024-04-08 19:52| 来源: 网络整理| 查看: 265

C++11开发中的Atomic原子操作

Nicol的博客铭

原文  https://taozj.org/2016/09/C-11%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84Atomic%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/

主题 C++

原子操作在多线程开发中经常用到,比如在计数器,序列产生器等地方,这类情况下数据有并发的危险,但是用锁去保护又显得有些浪费,所以原子类型操作十分的方便。

原子操作虽然用起来简单,但是其背景远比我们想象的要复杂。其主要在于现代计算系统过于的复杂:多处理器、多核处理器、处理器又有核心独有以及核心共享的多级缓存,在这种情况下,一个核心修改了某个变量,其他核心什么时候可见是一个十分严肃的问题。同时在极致最求性能的时代,处理器和编译器往往表现的很智能,进行极度的优化,比如什么乱序执行、指令重排等,虽然可以在当前上下文中做到很好的优化,但是放在多核环境下常常会引出新的问题来,这时候就必须提示编译器和处理器某种提示,告诉某些代码的执行顺序不能被优化。

所以这里说到的原子操作,基本都包含我们三个方面所关心的语义:操作本身是不可分割的(Atomicity),一个线程对某个数据的操作何时对另外一个线程可见(Visibility),执行的顺序是否可以被重排(Ordering)。

一、legacy GCC __sync

据说在C++11标准出来之前,大家都诟病C++标准没有一个明确的内存模型,随着多线程开发的普及这个问题显得越来越迫切。当然各个C++编译器实现者也是各自为政,GCC自然是实用主义当道,于是根据Intel的开发手册老早就搞出了一系列的__sync原子操作函数集合,这也是被广大程序员最为熟悉常用的操作了吧,罗列如下:

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); }

其实这里的1可以是任何non-zero的值,主要是用作bool的效果。

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

上面GCC那种full barrier的操作确实有效,但是就像当初系统内核从单核切换到多核用大颗粒锁一样的简单粗暴,先不说这种形势下编译器和处理器无法进行优化,光要变量使其对他处理器可见,就需要在处理间进行硬件级别的同步,显然是十分耗费资源的。在C++11新标准中规定的内存模型(memory model)颗粒要细化的多,如果熟悉这些内存模型,在保证业务正确的同时可以将对性能的影响减弱到最低。

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

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

2.1 Sequentially Consistent

该模型是最强的同步模式,参数表示为std::memory_order_seq_cst,同时也是默认的模型。

-Thread 1- -Thread2- y = 1if(x.load() ==2) x.store (2); assert (y ==1)

对于上面的例子,即使x和y是不相关的,通常情况下处理器或者编译器可能会对其访问进行重排,但是在seq_cst模式下,x.store(2)之前的所有memory accesses都会happens-before在这次store操作。

另外一个角度来说:对于seq_cst模式下的操作,所有memory accesses操作的重排不允许跨域这个操作,同时这个限制是双向的。

2.2 Acquire/Release

GCC的wiki可能讲的不太清楚,查看下面的典型Acquire/Release的使用例子:

std::atomic a{0}; intb =0; -Thread 1- b = 1; a.store(1, memory_order_release); -Thread 2- while(a.load(memory_order_acquire) !=1)/*waiting*/; std::cout


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有