C++11 并发指南七(C++11 内存模型一:介绍)

news2024/9/9 8:04:47

C++11 并发指南七(C++11 内存模型一:介绍)

文章目录

  • C++11 并发指南七(C++11 内存模型一:介绍)

第六章主要介绍了 C++11 中的原子类型及其相关的API,原子类型的大多数 API 都需要程序员提供一个 std::memory_order(可译为内存序,访存顺序) 的枚举类型值作为参数,比如:atomic_store,atomic_load,atomic_exchange,atomic_compare_exchange 等 API 的最后一个形参为 std::memory_order order,默认值是 std::memory_order_seq_cst(顺序一致性)。那么究竟什么是 std::memory_order 呢,为了解答这个问题,我们先来讨论 C++11 的内存模型。

一般来讲,内存模型可分为静态内存模型和动态内存模型,静态内存模型主要涉及类的对象在内存中是如何存放的,即从结构(structural)方面来看一个对象在内存中的布局,以一个简单的例子为例(截图参考《C++ Concurrency In Action》 P105 ):

img

上面是一个简单的 C++ 类(又称POD: Plain Old Data,它没有虚函数,没有继承),它在内存中的布局如图右边所示(对于复杂类对象的内存布局,请参考《深度探索C++对象模型》一书)。

动态内存模型可理解为存储一致性模型,主要是从行为(behavioral)方面来看多个线程对同一个对象同时(读写)操作时(concurrency)所做的约束,动态内存模型理解起来稍微复杂一些,涉及了内存,Cache,CPU 各个层次的交互,尤其是在共享存储系统中,为了保证程序执行的正确性,就需要对访存事件施加严格的限制。

文献中常见的存储一致性模型包括顺序一致性模型,处理器一致性模型,弱一致性模型,释放一致性模型,急切更新释放一致性模型、懒惰更新释放一致性模型,域一致性模型以及单项一致性模型。不同的存储一致性模型对访存事件次序的限制不同,因而对程序员的要求和所得到的的性能也不一样。存储一致性模型对访存事件次序施加的限制越弱,我们就越有利于提高程序的性能,但编程实现上更困难。

顺序一致性模型由 Lamport 于 1979 年提出。顺序一致性模型最好理解但代价太大,原文指出:

… the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.

该模型指出:如果在共享存储系统中多机并行执行的结果等于把每一个处理器所执行的指令流按照某种方式顺序地交织在一起在单机上执行的结果,则该共享存储系统是顺序一致性的。

顺序一致性不仅在共享存储系统上适用,在多处理器和多线程环境下也同样适用。而在多处理器和多线程环境下理解顺序一致性包括两个方面,(1). 从多个线程平行角度来看,程序最终的执行结果相当于多个线程某种交织执行的结果,(2)从单个线程内部执行顺序来看,该线程中的指令是按照程序事先已规定的顺序执行的(即不考虑运行时 CPU 乱序执行和 Memory Reorder)。

我们以一个具体的例子来理解顺序一致性:

假设存在两个共享变量a, b,初始值均为 0,两个线程运行不同的指令,如下表格所示,线程 1 设置 a 的值为 1,然后设置 R1 的值为 b,线程 2 设置 b 的值为 2,并设置 R2 的值为 a,请问在不加任何锁或者其他同步措施的情况下,R1,R2 的最终结果会是多少?

image-20230127230454161

由于没有施加任何同步限制,两个线程将会交织执行,但交织执行时指令不发生重排,即线程 1 中的 a = 1 始终在 R1 = b 之前执行,而线程 2 中的 b = 2 始终在 R2 = a 之前执行 ,因此可能的执行序列共有 4!/(2!*2!) = 6 种:

image-20230127230522253

上面的表格列举了两个线程交织执行时所有可能的执行序列,我们发现,R1,R2 最终结果只有 3 种情况,分别是 R1 == 0, R2 == 1(情况 1),R1 == 2, R2 == 0(情况2) 和 R1 == 2, R2 == 1(情况 3, 4, 5,6)。结合上面的例子,我想大家应该理解了什么是顺序一致性。

因此,多线程环境下顺序一致性包括两个方面,(1). 从多个线程平行角度来看,程序最终的执行结果相当于多个线程某种交织执行的结果,(2)从单个线程内部执行顺序来看,该线程中的指令是按照程序事先已规定的顺序执行的(即不考虑运行时 CPU 乱序执行和 Memory Reorder)。

当然,顺序一致性代价太大,不利于程序的优化,现在的编译器在编译程序时通常将指令重新排序(当然前提是保证程序的执行结果是正确的),例如,如果两个变量读写互不相关,编译器有可能将读操作提前(暂且称为预读prefetch 吧),或者尽可能延迟写操作,假设如下面的代码

int a = 1, b = 2;

void func()
{
    a = b + 22;
    b = 22;
}

在GCC 4.4 (X86-64)编译条件下,优化选项为 -O0 时,汇编后关键代码如下:

movl    b(%rip), %eax ; 将 b 读入 %eax
addl    $22, %eax ; %eax 加 22, 即 b + 22
movl    %eax, a(%rip) ; % 将 %eax 写回至 a, 即 a = b + 22
movl    $22, b(%rip) ; 设置 b = 22

而在设置 -O2 选项时,汇编后的关键代码如下:

movl    b(%rip), %eax ; 将 b 读入 %eax
movl    $22, b(%rip) ; b = 22
addl    $22, %eax ; %eax 加 22
movl    %eax, a(%rip) ; 将 b + 22 的值写入 a,即 a = b + 2

由上面的例子可以看出,编译器在不同的优化级别下确实对指令进行了不同程度重排,在 -O0(不作优化)的情况下,汇编指令和 C 源代码的逻辑相同,但是在 -O2 优化级别下,汇编指令和原始代码的执行逻辑不同,由汇编代码可以观察出,b = 22 首先执行,最后才是 a = b + 2, 由此看出,编译器会根据不同的优化等级来适当地对指令进行重排。在单线程条件下上述指令重排不会对执行结果带来任何影响,但是在多线程环境下就不一定了。如果另外一个线程依赖 a,b的值来选择它的执行逻辑,那么上述重排将会产生严重问题。编译器优化是一门深奥的技术,但是无论编译器怎么优化,都需要对优化条件作出约束,尤其是在多线程条件下,不能无理由地优化,更不能错误地优化。

另外,现代的 CPU 大都支持多发射和乱序执行,在乱序执行时,指令被执行的逻辑可能和程序汇编指令的逻辑不一致,在单线程条件下,CPU 的乱序执行不会带来大问题,但是在多核多线程时代,当多线程共享某一变量时,不同线程对共享变量的读写就应该格外小心,不适当的乱序执行可能导致程序运行错误。因此,CPU 的乱序执行也需要作出适当的约束。

综上所述,我们必须对编译器和 CPU 作出一定的约束才能合理正确地优化你的程序,那么这个约束是什么呢?答曰:内存模型。C++程序员要想写出高性能的多线程程序必须理解内存模型,编译器会给你的程序做优化(静态),CPU为了提升性能也有乱序执行(动态),总之,程序在最终执行时并不会按照你之前的原始代码顺序来执行,因此内存模型是程序员、编译器,CPU 之间的契约,遵守契约后大家就各自做优化,从而尽可能提高程序的性能。

C++11 中规定了 6 中访存次序(Memory Order),如下:

[复制代码](javascript:void(0)😉

enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

[复制代码](javascript:void(0)😉

std::memory_order 规定了普通访存操作和相邻的原子访存操作之间的次序是如何安排的,在多核系统中,当多个线程同时读写多个变量时,其中的某个线程所看到的变量值的改变顺序可能和其他线程写入变量值的次序不相同。同时,不同的线程所观察到的某变量被修改次序也可能不相同。然而,如果保证所有对原子变量的操作都是顺序的话,可能对程序的性能影响很大,因此,我们可以通过 std::memory_order 来指定编译器对访存次序所做的限制。因此,在原子类型的 API 中,我们可以通过额外的参数指定该原子操作的访存次序(内存序),默认的内存序是 std::memory_order_seq_cst

我们可以把上述 6 中访存次序(内存序)分为 3 类,顺序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,) 和 Relax 模型(std::memory_order_relaxed)。三种不同的内存模型在不同类型的 CPU上(如 X86,ARM,PowerPC等)所带来的代价也不一样。例如,在 X86 或者 X86-64平台下,Acquire-Release 类型的访存序不需要额外的指令来保证原子性,即使顺序一致性类型操作也只需要在写操作(Store)时施加少量的限制,而在读操作(Load)则不需要花费额外的代价来保证原子性。

===================================== TL;DR =====================================

附:本文剩余部分将介绍其他的存储器一致模型中的其他几种较常见的模型:处理器一致性(Processor Consistency)模型,弱一致性(Weak Consistency)模型,释放一致性(Release Consistency)模型。[注:以下内容来自中国科学院计算技术研究所胡伟武老师写的《计算机体系结构》(清华大学出版社),该书是胡伟武老师给研究生讲课所用的教材,本文略有删改]

处理器一致性(Processor Consistency)模型:处理器一致性(Processor Consistency)模型比顺序一致性模型弱,因此对于某些在顺序一致性模型下能够正确执行的程序在处理器一致性条件下执行时可能会导致错误的结果,处理器一致性模型对访存事件发生次序施加的限制是:(1). 在任意读操作(Load)被允许执行之前,所有在同一处理器中先于这一 Load 的读操作都已完成;(2). 在任意写操作(Store)被允许执行之前,所有在同一处理器中先于这一 Store 的访存操作(包括 Load 和 Store操作)都已完成。上述条件允许 Store 之后的 Load 越过 Store 操作而有限执行。

弱一致性(Weak Consistency)模型:弱一致性(Weak Consistency)模型的主要思想是将同步操作和普通的访存操作区分开来,程序员必须用硬件可识别的同步操作把对可写共享单元的访存保护起来,以保证多个处理器对可写单元的访问是互斥的。弱一致性对访存事件发生次序的限制如下:(1). 同步操作的执行满足顺序一致性条件; (2). 在任一普通访存操作被允许执行之前,所有在同一处理器中先于这一访存操作的同步操作都已完成; (3). 在任一同步操作被允许执行之前,所有在同一处理器中先于这一同步操作的普通操作都已完成。上述条件允许在同步操作之间的普通访存操作执行时不用考虑进程之间的相关,虽然弱一致性增加了程序员的负担,但是它能有效地提高系统的性能。

释放一致性(Release Consistency)模型:释放一致性(Release Consistency)模型是对弱一致性(Weak Consistency)模型的改进,它把同步操作进一步分成了获取操作(Acquire)和释放操作(Release)。Acquire 用于获取对某些共享变量的独占访问权,而 Release 则用于释放这种访问权,释放一致性(Release Consistency)模型访存事件发生次序的限制如下:(1). 同步操作的执行满足顺序一致性条件; (2). 在任一普通访存操作被允许执行之前,所有在同一处理器中先于这一访存操作的 Acquire 操作都已完成; (3). 在任一 Release 操作被允许执行之前,所有在同一处理器中先于这一 Release 操作的普通操作都已完成。

在硬件实现的释放一致性模型中,对共享单元的访存是及时进行的,并在执行获取操作(Acquire)和释放操作(Release)时对齐。在共享虚拟存储系统或者在由软件维护的数据一致性的共享存储系统中,由于通信和数据交换的开销很大,有必要减少通信和数据交换的次数。为此,人们在释放一致性(Release Consistency)模型的基础上提出了急切更新释放一致性模型(Eager Release Consistency)和懒惰更新释放一致性模型(Lazy Release Consistency)。在急切更新释放一致性模型中,在临界区内的多个存数操作对共享内存的更新不是及时进行的,而是在执行 Release 操作之前(即退出临界区之前)集中进行,把多个存数操作合并在一起统一执行,从而减少了通信次数。而在懒惰更新释放一致性模型中,由一个处理器对某单元的存数操作并不是由此处理器主动传播到所有共享该单元的其他处理器,而是在其他处理器要用到此处理器所写的数据时(即其他处理器执行 Acquire 操作时)再向此处理器索取该单元的最新备份,这样可以进一步减少通信量。

===============================================================================

作时)再向此处理器索取该单元的最新备份,这样可以进一步减少通信量。

===============================================================================

好了,本文主要介绍了内存模型的相关概念,并重点介绍了顺序一致性模型(附带介绍了几种常见的存储一致性模型),并以一个实际的小例子向大家介绍了为什么程序员需要理解内存模型,总之,C++ 程序员要想写出高性能的多线程程序必须理解内存模型,因为编译器会给你的程序做优化(如指令重排等),CPU 为了提升性能也有多发射和乱序执行,因此程序在最终执行时并不会按照你之前的原始代码顺序来执行,所以内存模型是程序员、编译器,CPU 之间的契约,遵守契约后大家就各自做优化,从而尽可能提高程序的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/183114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试第一次被问到SDK测试,当时就懵了

01、是什么 客户端SDK是为第三方开发者提供的软件开发工具包,包括SDK接口、开发文档和Demo示例等。SDK和应用之间是什么关系呢?以云信即时消息服务为例,如下图所示,应用客户端通过调用云信SDK接口,进行消息等数据查询…

[前端笔记——CSS] 14.图像、媒体和表单元素

[前端笔记——CSS] 14.图像、媒体和表单元素1.CSS调整大小2.图片、媒体和表单元素2.1替换元素2.2 form元素2.3 举个实例1.CSS调整大小 一个空的<div>是没有尺寸的。如果在 HTML 文件中添加一个空<div> 并给予其边框&#xff0c;则会在页面上看到一条线。 <div…

【软件安装】ubuntu安装nvidia驱动,解决闪屏问题

Official Drivers | NVIDIA nvidia驱动下载官网&#xff0c;登陆速度是有点慢的&#xff0c;需要耐心等待 我的电脑是笔记本&#xff0c;ubuntu18.04.6&#xff0c;NVIDIA GeForce RTX 3050 Laptop GPU 天时地利人和的情况下&#xff0c;按照下面这篇博文就可以顺利安装nvidi…

【存储】etcd的存储是如何实现的(2)

在上一篇中&#xff0c;介绍了etcd底层存储的内容&#xff0c;包括wal、raft.MemoryStorage以及backend。在介绍backend时提到了backend只是etcd kv存储的一部分&#xff0c;负责持久化存储&#xff0c;backend加内存化treeIndex才构成etcd完整的支持mvcc的kv存储。所以这篇就来…

红队大量资产指纹探测工具和摄像头漏-洞渗-透和利用工具

红队大量资产指纹探测工具和摄像头漏-洞渗-透和利用工具。 Finger定位于一款红队在大量的资产中存活探测与重点攻-击系统指纹探测工具。在面临大量资产时候Finger可以快速从中查找出重点攻-击系统协助我们快速展开渗-透。 实际效果 URL批量扫描效果如下: 调用api进行资产收集效…

第九层(6):STL之queue

文章目录前情回顾queue概念queue容器需要注意的地方queue类内的构造函数queue类内的赋值操作queue类内的插入操作queue类内的删除操作queue类内的访问queue类内的大小操作下一座石碑&#x1f389;welcome&#x1f389; ✒️博主介绍&#xff1a;一名大一的智能制造专业学生&…

设计模式第4式:观察者模式Spring事件监听机制

前言 观察者模式是一种非常重要的设计模式&#xff0c;在JDK和Spring源码中使用非常广泛&#xff0c;而且消息队列软件如kafka、rocketmq等也应用了观察者模式。那么我们就很有必要学习一下观察者模式了。 随后我们来看看大名鼎鼎的事件监听机制&#xff0c;它是基于观察者模…

直波导与微环的耦合——Lumerical仿真1

微环与直波导的耦合的Lumerical仿真的一个记录&#xff0c;包括仿真步骤和一些问题的探究&#xff0c;参考自https://www.bilibili.com/video/BV1tF411z714。 &#x1f381;附Lumerical仿真文件 Lumerical第一个仿真一、建立结构1、放置微环结构2、修改结构二、光源1、放置光源…

React是不是MVVM架构?

首先说结论&#xff1a;不是 一、MVVM Model-View-ViewModel&#xff1a;一句话概括MVVM&#xff0c;操作数据&#xff0c;就是操作视图&#xff0c;就是操作DOM。开发者只需要完成包含申明绑定的视图模板&#xff0c;编写ViewModel中业务数据变更逻辑&#xff0c;View层则完…

MySQL(六):redo日志——保证事务的持久性

目录一、redo日志的基本介绍1.1 什么是redo日志1.2 redo日志的格式1.3 redo日志的类型1.4 Mini-Transaction二、redo日志的写入过程2.1 redo log block2.2 redo日志缓冲区2.3 将redo日志写入log buffer2.3 redo日志刷新到磁盘2.5 redo日志文件组2.6 redo日志文件格式2.7 log se…

【MySQL进阶】MySQL触发器详解

序号系列文章7【MySQL基础】运算符及相关函数详解8【MySQL基础】MySQL多表操作详解9【MySQL进阶】MySQL事务详解10【MySQL进阶】MySQL视图详解文章目录前言1&#xff0c;触发器1.1&#xff0c;触发器概述1.2&#xff0c;触发器使用环境2&#xff0c;触发器基本操作2.1&#xff…

CentOS7 安装单机 Kafka

一、单机安装 1、上传压缩文件到服务器、解压 tar -zxvf kafka_2.13-3.3.2.tgz -C /usr/local #解压到 usr/local目录下 进入解压目录下 更名kafka mv kafka_2.13-3.3.2/ kafka-3.3.2 2、配置环境变量 vim /etc/profile export KAFKA_HOME/usr/local/kafka-3.3.2export PATH$PA…

MySQL学习记录(9)InnoDB存储引擎

文章目录6、InnoDB存储引擎6.1、逻辑存储结构6.2、架构6.2.1、概述6.2.2、内存结构6.2.3、磁盘结构6.2.4、后台线程6.3、事务原理6.3.1、事务基础6.3.2、redo log日志6.3.3、undo log日志6.4、MVCC6.4.1、基本概念6.4.2、记录中隐藏字段6.4.3、undo log日志6.4.4、readview6.4.…

【Pytorch项目实战】之图像分类与识别:手写数字识别(MNIST)、普适物体识别(CIFAR-10)

文章目录图像分类与识别&#xff08;一&#xff09;实战&#xff1a;基于CNN的手写数字识别&#xff08;数据集&#xff1a;MNIST&#xff09;&#xff08;二&#xff09;实战&#xff1a;基于CNN的图像分类&#xff08;数据集&#xff1a;CIFAR-10&#xff09;图像分类与识别 …

Lua 函数 - 可变参数

Lua 函数 - 可变参数 参考至菜鸟教程。 Lua函数可以接收可变数目的参数&#xff0c;和C语言类似&#xff0c;在函数参数列表中使用三点...表示函数有可变的参数。 function add(...) local s 0 for i, v in ipairs{...} do --> {...} 表示一个由所有变长参数构成的数…

模拟实现C库函数(2)

"烦恼无影踪,丢宇宙~"上一篇的模拟实现了好几个库函数,strlen\strcpy\memcpy\memmove,那么这一篇又会增加几个常用C库函数的模拟实现 memset\itoa\atoi。一、memsetmemset - fill memory with a constant byte#include <string.h>void *memset(void *s, int c,…

机器自动翻译古文拼音 - 十大宋词 - 江城子·乙卯正月二十日夜记梦 苏轼

江城子乙卯正月二十日夜记梦 宋苏轼 十年生死两茫茫&#xff0c;不思量&#xff0c;自难忘。 千里孤坟&#xff0c;无处话凄凉。 纵使相逢应不识&#xff0c;尘满面&#xff0c;鬓如霜。 夜来幽梦忽还乡&#xff0c;小轩窗&#xff0c;正梳妆。 相顾无言&#xff0c;惟有泪千…

uniapp使用及踩坑项目记录

环境准备 下载 HBuilderX 使用命令行创建项目&#xff1a; 一些常识准备 响应式单位rpx 当设计稿宽度为750px的时&#xff0c;1rpx1px。 uniapp中vue文件style不用添加scoped 打包成h5端的时候自动添加上去&#xff0c;打包成 微信小程序端 不需要添加 scoped。 图片的…

SpringDataJpa set()方法自动保存失效

问题描述&#xff1a;springdatajpa支持直接操作对象设置属性进行更新数据库记录的方式&#xff0c;正常情况下&#xff0c;get()得到的对象直接进行set后&#xff0c;即使不进行save操作&#xff0c;也将自动更新数据记录&#xff0c;将改动持久化到数据库中&#xff0c;但这里…

20230126使AIO-3568J开发板在原厂Android11下跑起来

20230126使AIO-3568J开发板在原厂Android11下跑起来 2023/1/26 18:22 1、前提 2、修改dts设备树 3、适配板子的dts 4、&#xff08;修改uboot&#xff09;编译系统烧入固件验证 前提 因源码是直接使用原厂的SDK&#xff0c;没有使用firefly配套的SDK源码&#xff0c;所以手上这…