当前位置: 首页 > article >正文

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

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

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

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


【C++11 新标准中的内存模型】

在 C++11 新标准中规定的内存模型(Memory Model)颗粒要细化的多,如果熟悉这些内存模型,能够在保证业务正确的同时可以将对性能的影响减弱到最低。

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

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

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、Acquire / Release

std::atomic<int> 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 << b << '\n';

毫无疑问,如果是 seq_cst,那么上面的操作一定是成功的(打印变量 b 显示为 1)。

  • memory_order_release 保证在这个操作之前的 memory accesses 不会重排到这个操作之后去,但是这个操作之后的 memory accesses 可能会重排到这个操作之前去。通常这个主要是用于之前准备某些资源后,通过 store+memory_order_release 的方式 “Release” 给别的线程。
  • memory_order_acquire 保证在这个操作之后的 memory accesses 不会重排到这个操作之前去,但是这个操作之前的 memory accesses 可能会重排到这个操作之后去。通常通过 load+memory_order_acquire 判断或者等待某个资源,一旦满足某个条件后就可以安全的 “Acquire” 消费这些资源了。

3、Consume

这是一个相比 Acquire / Release 更加宽松的内存模型,对非依赖的变量也去除了 happens-before 的限制,减少了所需同步的数据量,可以加快执行的速度。

-Thread 1-
n = 1
m = 1
p.store (&n, memory_order_release)

-Thread 2-
t = p.load (memory_order_acquire);
assert( *t == 1&& m ==1);

-Thread 3-
t = p.load (memory_order_consume);
assert( *t == 1&& m ==1);

线程 2 的 assert 会 pass,而线程 3 的 assert 可能会 fail,因为 n 出现在了 store 表达式中,算是一个依赖变量,会确保对该变量的 memory access 会 happends-before 在这个 store 之前,但是 m 没有依赖关系,所以不会同步该变量,对其值不作保证。

Comsume 模式因为降低了需要在硬件之间同步的数量,所以理论上其执行的速度会比之上面的内存模型块一些,尤其在共享内存大规模数据量情况下,应该会有较明显的差异表现出来。

在这里,Acquire / Consume~Release 这种线程间同步协作的机制就被完全暴露了,通常会形成 Acquired / Consume 来等待 Release 的某个状态更新。需要注意的是这样的通信需要两个线程间成对的使用才有意义,同时对于没有使用这个内存模型的第三方线程没有任何作用效果。


4、Relaxed

最宽松的模式memory_order_relaxed 没有 happens-before 的约束,编译器和处理器可以对 memory access 做任何的 re-order,因此另外的线程不能对其做任何的假设,这种模式下能做的唯一保证,就是一旦线程读到了变量 var 的最新值,那么这个线程将再也见不到 var 修改之前的值了。

这种情况通常是在需要原子变量,但是不在线程间同步共享数据的时候会用,同时当 relaxed 存一个数据的时候,另外的线程将需要一个时间才能 relaxed 读到该值,在非缓存一致性的构架上需要刷新缓存。在开发的时候,如果你的上下文没有共享的变量需要在线程间同步,选用 Relaxed 就可以了


5、小结

看到这里,对 Atomic 原子操作就不仅仅停留在 indivisable 的层次了,因为所有的内存模型都能保证对变量的修改是原子的,C++11 新标准的原子应该上升到了线程间数据同步和协作的问题了,跟前面的 LockFree 关系也比较密切。

手册上也这样告诫我们:除非你知道这是什么,需要减弱线程间原子上下文同步的耦合性增加执行效率,才考虑这里的内存模型来优化你的程序,否则还是老老实实的使用默认的 memory_order_seq_cst,虽然速度可能会慢点,但是稳妥些,万一由于你不成熟的优化带来问题,是很难去调试的。


http://www.kler.cn/news/343180.html

相关文章:

  • react 封装防抖
  • 【优选算法】--- 分治 快速排序
  • 如何快速给word文件加拼音?请跟着步骤完成吧
  • centos7更新yum国内源
  • 【hot100-java】合并 K 个升序链表
  • 【含文档】基于Springboot+Vue的小区家政服务预约平台(含源码+数据库+lw)
  • CST软件优化超表面--- 偏振片- 线圆极化转换,天线阵任务,远场算轴比AR
  • 学习之偏函数
  • 数据挖掘学习笔记:朴素贝叶斯 | Python复现
  • 搬砖10、Python 图形用户界面和游戏开发
  • java:pdfbox 3.0 去除扫描版PDF中文本水印
  • 电子取证新视角:Windows系统中文用户输入痕迹信息提取方法研究与实现
  • 使用Qt Creator创建项目
  • 腾讯云SDK用量统计
  • SpringBoot开发——整合Actuator监控和管理Spring Boot 应用
  • vuex安装报错
  • v-html里面的标签设置样式没有用怎么办?
  • 尚硅谷rabbitmq 2024第30-33节 死信队列 答疑
  • Linux:进程调度算法和进程地址空间
  • No.9 笔记 | PHP学习指南:从入门到精通的要点总结