转载自:https://www.dazhuanlan.com/2019/12/14/5df40323d6adf/

原子操作(atomic)是无锁编程(Lock-Free Programming)的基础。C++内存模型规定了多个线程访问同一个内存地址时的语义,以及某个线程对内存地址的更新何时能被其它线程看见。

该模型的核心思想就是由程序员用同步原语(例如,锁或者C++11中新引入的atomic类型的共享变量)来保证你程序是没有数据竞争的,这样CPU和编译器就会保证程序是按程序员所想的那样执行的(即顺序一致性)。

C++编译器可能会进行一些错误优化从而导致数据竞争。第二,绝大多数情况下线程库能正确的完成任务,而在极少数对性能有更高要求的情况下(尤其是需要利用底层的硬件特性来实现高性能Lock Free算法时)需要更精确的内存模型以规定好程序的行为

C++11中的内存屏障

松散”内存屏障

std::memory_order_relaxed,松散操作,没有同步或顺序制约,仅对此操作要求原子性。带标签 memory_order_relaxed的原子操作无同步操作;它们不会在并发的内存访问间强加顺序。它们只保证原子性和修改顺序一致性。

例如,对于最初为零的xy:

1

2

3

4

5

6

7

r1 = y.load(memory_order_relaxed); // A

x.store(r1, memory_order_relaxed); // B

// 线程 2 :

r2 = x.load(memory_order_relaxed); // C

y.store(42, memory_order_relaxed); // D

允许产生结果r1 == 42 && r2 == 42,因为即使线程1中A先序于B且线程2中C先序于D ,却没有制约避免y的修改顺序中D先出现于A ,而x的修改顺序中B先出现于C。D在y上的副效应,可能可见于线程1中的加载A ,同时B在x上的副效应,可能可见于线程2中的加载C。

如上图所示,假设:a, b, c分别为普通变量,而x为atomic类型的变量,当在写入x时,设置memory_order_relaxed时,写入a,写入b的顺序,在另外的线程上看到的,完全可能是相互调换的,写入c的位置,完全也有可能由出现在写入x之前,而在另外一个线程上看到的,确实在写入x之后,同时,读出b的动作,完全有可能从写入x之后,编程在另外一个线程上看,是在写入x之前。也就是,各种乱序, 都是被允许的。

松散内存顺序的典型使用是计数器自增,只要求原子性,但不要求顺序或同步

单向的”释放”内存屏障

std::memory_order_release,释放操作,当前线程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于加载该同一原子变量的其他线程。

如上图所示,在使用memory_order_release设置的原子操作写入的情况下,原子操作之后的读写操作,在另外的线程(加载了该原子变量的线程)看来,可以重排到原子操作之前(通俗点说,就是另外线程可能认为当前线程是先执行的写入的a操作,再执行的写入x操作)。但是,如下图所示,原子操作之前的读、写操作,计算机系统必须保证,在另外一个线程看来,他是在原子操作写入之前就被写入了, 不能是在原子操作之后才写入(通俗点说,也就是另外一个线程,不能认为当前线程先写入的x,再写入的c,他一定要看到的是先写入c,再写入的x):

单向的”加载”内存屏障

std::memory_order_acquire,加载操作,当前线程中读或写不能被重排到此加载前。其他释放同一原子变量的线程的所有写入,为当前线程所可见。

如上图所示,在设置memory_order_acquire的对x的读操作前的读、写操作,在另外的线程看来(或者说实际的运行顺序),可以被重排到对x的读操作后,(通俗点说,就是其他线程可以是认为当前线程先读取的x,再写入的c)。然而如下图所示,对x的读操作后的读、写操作,在另外的线程看来不允许被重排到x的读操作前(不允许其他线程看到当前线程先写入的a,再读取的x)。

单向“加载”+单向“释放”协议

往往memory_order_acquirememory_order_release是配合着一起使用的:

  1. 线程1使用memory_order_release写入原子变量x
  2. 线程2使用memory_order_acquire读出原子变量x

所有在线程1上,在写入x之前的写入操作,都将在线程2上,在读出x之后,被看到。使用单向“加载”+单向“释放”协议的场景往往是:

  1. 线程1,写入一些实际数据,接着通过将原子变量x设置为某个值A(通过使用memory_order_release写入原子变量x)来“发布”这些数据。
  2. 线程2,通过读取并判断x已被设置为A(通过使用memory_order_acquire来读取原子变量x),进而读取线程1实际“发布”的那些数据

如上图所示,thread_1在release写入x(值:A)之前,写入了待发布的a,b的数据,而thread_2,将在acquire读出x且为A之后,将读到thread_1发布的a,b的数据。同时,我们可以注意到,在thread_2上,在acquire读出x之前,如果对a进行读操作,我们是无法确认读到的a一定会thread_1在之前最后写入的a,这里的顺序是不会被保证的,重排是被允许的。同时,在之后,读取c,读到的是否为thread_1最后写入的c,也是不确定的,因为,在x写入之后,thread_1上又出现了一次写入,而如果在此之前,还有一次写入, 这两次写入之间,是不存在限制,可能会被重排的。

thread_1上有了release_store,对于a,b的写入就一定会在x的改变之前,在thread_2上,就不会出现类似读出c,的不确定性。thread_2上有了acquire_load,右侧的读出a,就不会被重排读到左侧,而左侧读出a的不确定性,也不存在。thread_1卡住的是:对于数据a,b的写入不能排到x的写入之后,thread_2卡住的,是对于数据a,b的读取,不能排到读取x之前,这样,就保证了数据a,b,与”信号量”x,之间,在thread_1, thread_2上的同步关系。

双向的”加载-释放”内存屏障

std::memory_order_acq_rel,加载-释放操作,带此内存顺序的读-修改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后,当然,具有这一限制的前提是,观察读写顺序的不同线程是使用的同一个原子变量并基于这一内存顺序。

最严的”顺序一致”内存屏障

std::memory_order_seq_cst,顺序一致操作,此内存顺序的操作既是加载操作又是释放操作,没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。带标签memory_order_seq_cst的原子操作不仅以与释放/加载顺序相同的方式排序内存(在一个线程中先发生于存储的任何结果都变成做加载的线程中的可见),还对所有拥有此标签的内存操作建立一个单独全序。memory_order_seq_cstmemory_order_acq_rel更强,memory_order_acq_rel的顺序保障,是要基于同一个原子变量的,也就是说,在这个原子变量之前的读写,不能重排到这个原子变量之后,同时这个原子变量之后的读写,也不能重排到这个原子变量之前。但是,如果两个线程基于memory_order_acq_rel使用了两个不同的原子变量x1, x2,那在x1之前的读写,重排到x2之后,是完全可能的,在x1之后的读写,重排到x2之前,也是被允许的。然而,如果两个原子变量x1,x2,是基于memory_order_seq_cst在操作,那么即使是x1之前的读写,也不能被重排到x2之后,x1之后的读写,也不能重排到x2之前,也就说,如果都用memory_order_seq_cst,那么程序代码顺序(Program Order)就将会是你在多个线程上都实际观察到的顺序(Observed Order)

c++内存管理-内存顺序相关推荐

  1. LwIP 之六 详解动态内存管理 内存池(memp.c/h)

      该文主要是接上一部分LwIP 之 详解动态内存管理 内存堆(mem.c/h),该部分许多内容需要用到上一篇的内容.该部分主要是详细介绍LwIP中的动态内存池.整个内存池的实现相较于内存堆来说,还是 ...

  2. LwIP 之五 详解动态内存管理 内存堆(mem.c/h)

    写在前面   目前网上有很多介绍LwIP内存的文章,但是绝大多数都不够详细,甚至很多介绍都是错误的!无论是代码的说明还是给出的图例,都欠佳!下面就从源代码,到图例详细进行说明.   目前,网络上多数文 ...

  3. 内存管理-内存池的实现

    内存池的实现 1 前言 2 内存池的原理 2.1 内存利用链表进行管理 2.2 分配固定大小 2.3 按块进行内存管理 3 内存池的实现 3.1 内存池的创建 3.2 内存池的销毁 3.3 内存分配 ...

  4. dpdk内存管理——内存初始化

    *说明:本系列博文源代码均来自dpdk17.02* 1.1内存初始化 1.1.1 hugepage技术 hugepage(2M/1G..)相对于普通的page(4K)来说有几个特点: (1) huge ...

  5. 11 操作系统第三章 内存管理 内存的基本知识 内存管理 内存空间扩充 连续分配管理方式

    文章目录 1 内存概念 1.1 内存作用 1.2 逻辑地址VS物理地址 1.3 装入的三种方式 1.3.1 绝对装入 1.3.2 可重定位装入 1.3.3 动态重定位装入 1.4 链接的三种方式 1. ...

  6. Linux内存管理内存映射以及通过反汇编定位内存错误问题

    提到C语言,我们知道C语言和其他高级语言的最大的区别就是C语言是要操作内存的! 我们需要知道--变量,其实是内存地址的一个抽像名字罢了.在静态编译的程序中,所有的变量名都会在编译时被转成内存地址.机器 ...

  7. Linux内核:内存管理——内存回收

    概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文件页进行的.对于匿名页,内存回收过程中会筛选出一些不经常使用的匿名页,将它们写入到swa ...

  8. 【Linux 内核 内存管理】Linux 内核堆内存管理 ① ( 堆内存管理 | 内存描述符 mm_struct 结构体 | mm_struct 结构体中的 start_brk、brk 成员 )

    文章目录 一.堆内存管理 二.内存描述符 mm_struct 结构体 三.mm_struct 结构体中的 start_brk.brk 成员 一.堆内存管理 Linux 操作系统中的 " 堆内 ...

  9. Android之内存管理-内存监测-内存优化

    推荐文章:Android进程与内存及内存泄漏 Android之内存管理 1.1 Dalvik Dalvik虚拟机是Android程序的虚拟机,是Android中Java程序的运行基础.其指令集基于寄存 ...

  10. 纯干货,linux内存管理——内存管理架构(建议收藏)

    一.内存管理架构 内存管理子系统架构可以分为:用户空间.内核空间及硬件部分3个层面,具体结构如下所示: 1.用户空间:应用程序使用malloc()申请内存资源/free()释放内存资源. 2.内核空间 ...

最新文章

  1. python编程之slice与indices函数用法
  2. linux dev urandom,Linux random vs urandom 区别
  3. CodeForces - 850C Arpa and a game with Mojtaba(博弈+sg函数)
  4. c# 读取记事本txt文档到DataTable中
  5. 计算机课中排序选什么,《计算机应用基础课件》1.6 排序复习课程.ppt
  6. mysql不兼容_mysql5.7 不兼容问题
  7. 多重背包单调队列优化思路_多重背包问题
  8. HTML文档包应含几个基本标记,HTML基本结与常用标记.doc
  9. 如何同时让多台服务器安装系统,如何同时安装多台服务器?
  10. C#获取二维数组的行数和列数及其多维。。。
  11. Java 线程池学习
  12. 5.七个重点网络协议
  13. bootstrap 快速入门
  14. MAC使用RZ SZ指令实现本地与远程服务器之间文件上传与下载
  15. 《商务与经济统计》学习笔记(一)---数据与统计资料
  16. 企业宣传片解说词的写法指导。
  17. 用python.turtle画中国地图
  18. Collaborative Filtering with Temporal Dynamics
  19. 我被List中remove()方法的陷阱,坑惨了!
  20. 工单服务管理系统开发

热门文章

  1. weblogic常见漏洞
  2. iOS之深入解析objc_msgSend消息转发机制的底层原理
  3. iOS之深入解析类加载的底层原理:分类如何加载到类以及分类和类的配合使用
  4. 2020年第十一届蓝桥杯 - 省赛 - C/C++研究生组 - F.成绩分析
  5. 数据库开发——MySQL——慢查询优化
  6. The directory '*' or its parent directory is not owned by the current user
  7. linux系统添加新用户并赋予相应权限
  8. 排序算法 —— 选择排序
  9. Git《二》时光机穿梭
  10. 大数据WEB阶段(九)Myeclipse中配置Tomcat并发布项目