[笔记]C++并发编程实战 《五》C++内存模型和原子类型操作

news2024/9/20 19:29:32

文章目录

  • 前言
  • 第5章 C++内存模型和原子类型操作
  • 5.1 内存模型基础
    • 5.1.1 对象和内存位置
    • 5.1.2 对象、内存位置和并发
    • 5.1.3 修改顺序
  • 5.2 C++中的原子操作和原子类型
    • 5.2.1 标准原子类型
  • 总结


前言

第5章 C++内存模型和原子类型操作

本章主要内容:

  • C++11内存模型详解
  • 标准库提供的原子类型
  • 使用各种原子类型
  • 原子操作实现线程同步功能

C++标准中,有一个十分重要特性,常被程序员们所忽略。它不是一个新语法特性,也不是新工具,它就是多线程(感知)内存模型。

内存模型没有明确的定义基本部件应该如何工作的话,之前介绍的那些工具就无法正常工作。那为什么大多数程序员都没有注意到它呢?

当使用互斥量保护数据和条件变量,或者是“期望”上的信号事件时,对于互斥量为什么能起到这样作用,大多数人不会去关心。只有当试图去“接触硬件”,才能详尽的了解到内存模型是如何起作用的。

C++是一个系统级别的编程语言,标准委员会的目标之一就是不需要比C++还要底层的高级语言。C++应该向程序员提供足够的灵活性,无障碍的去做他们想要做的事情;当需要的时候,可以让他们“接触硬件”。原子类型和原子操作就允许他们“接触硬件”,并提供底层级别的同步操作,通常会将常规指令数缩减到1~2个CPU指令。

本章,我们将讨论内存模型的基本知识,再了解一下原子类型和操作,最后了解与原子类型操作相关的各种同步。

这个过程会比较复杂:

  • 除非已经打算使用原子操作(比如,第7章的无锁数据结构)同步你的代码;否则,就没有必要了解过多的细节。

让我们先轻松愉快的来看一下有关内存模型的基本知识。

5.1 内存模型基础

内存模型:

  • 一方面是基本结构,这与内存布局的有关,
  • 另一方面就是并发。并发基本结构很重要,特别是低层原子操作。

所以我将会从基本结构讲起,C++所有的对象都和内存位置有关。

5.1.1 对象和内存位置

一个C++程序中所有数据都是由对象构成。

不是说创建一个int的衍生类,或者是基本类型中存在有成员函数,或是像在Smalltalk和Ruby语言那样——“一切都是对象”。对象仅仅是对C++数据构建块的声明。

C++标准定义类对象为“存储区域”,但对象还是可以将自己的特性赋予其他对象,比如:

  • 相应类型和生命周期。

像int或float这样的对象是基本类型。当然,也有用户定义类的实例。

一些对象(比如,数组,衍生类的实例,特殊(具有非静态数据成员)类的实例)拥有子对象,但是其他对象就没有。

无论对象是怎么样的类型,对象都会存储在一个或多个内存位置上。每个内存位置不是标量类型的对象,就是标量类型的子对象,比如,unsigned short、my_class*或序列中的相邻位域。

当使用位域时就需要注意:

  • 虽然相邻位域中是不同的对象,但仍视其为相同的内存位置。

如图5.1所示,将一个struct分解为多个对象,并且展示了每个对象的内存位置。
在这里插入图片描述
首先,完整的struct是一个有多个子对象(每一个成员变量)组成的对象。位域bf1和bf2共享同一个内存位置(int是4字节、32位类型),并且 std::string 类型的对象s由内部多个内存位置组成,但是其他的成员都拥有自己的内存位置。注意,位域宽度为0的bf3是如何与bf4分离,并拥有各自的内存位置的。

(译者注:图中bf3可能是一个错误展示,在C++和C中规定,宽度为0的一个未命名位域强制下一位域对齐到其下一type边界,其中type是该成员的类型。这里使用命名变量为0的位域,可能只是想展示其与bf4是如何分离的。有关位域的更多可以参考wiki的页面)。

这里有四个需要牢记的原则:

  1. 每一个变量都是一个对象,包括作为其成员变量的对象。
  2. 每个对象至少占有一个内存位置。
  3. 基本类型都有确定的内存位置(无论类型大小如何,即使他们是相邻的,或是数组的一部
    分)。
  4. 相邻位域是相同内存中的一部分。

我确定你会好奇,这些在并发中有什么作用?下面就让我们来见识一下。

5.1.2 对象、内存位置和并发

这部分对于C++的多线程来说是至关重要的:所有东西都在内存中。当两个线程访问不同的内存位置时,不会存在任何问题,一切都工作顺利。当两个线程访问同一个内存位置,就要小心了。如果没有线程更新数据,那还好;只读数据不需要保护或同步。当有线程对内存位置上的数据进行修改,那就有可能会产生条件竞争,就如第3章所述的那样。

为了避免条件竞争,两个线程就需要一定的执行顺序。第一种方式,如第3章所述,使用互斥量来确定访问的顺序;当同一互斥量在两个线程同时访问前被锁住,那么在同一时间内就只有一个线程能够访问到对应的内存位置,所以后一个访问必须在前一个访问之后。另一种是使用原子操作(详见5.2节中对于原子操作的定义),决定两个线程的访问顺序。

使用原子操作来规定顺序在5.3节中会有介绍。当多于两个线程访问同一个内存地址时,对每个访问这都需要定义一个顺序。

如果不规定两个不同线程对同一内存地址访问的顺序,那么访问就不是原子的;并且,当两个线程都是“作者”时,就会产生数据竞争和未定义行为。

以下的声明由为重要:未定义的行为是C++中最黑暗的角落。根据语言的标准,一旦应用中有任何未定义的行为,就很难预料会发生什么事情;因为,未定义行为是难以预料的。我就知道一个未定义行为的特定实例,让某人的显示器起火的案例。虽然,这种事情应该不会发生在你身上,但是数据竞争绝对是一个严重的错误,需要不惜一切代价避免它。

另一个重点是:当程序对同一内存地址中的数据访问存在竞争,可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回到定义行为的区域内。

在了解原子操作前,还有一个有关对象和内存地址的概念需要重点了解:

  • 修改顺序。

5.1.3 修改顺序

每个C++程序中的对象,都有(由程序中的所有线程对象)确定好的修改顺序,且在初始化开始阶段确定。大多数情况下,这个顺序不同于执行中的顺序,但在给定的程序中,所有线程都需要遵守这个顺序。如果对象不是一个原子类型(将在5.2节详述),必须确保有足够的同步操作,来确定每个线程都遵守了变量的修改顺序。当不同线程在不同序列中访问同一个值时,可能就会遇到数据竞争或未定义行为(详见5.1.2节)。如果使用原子操作,编译器就有责任去做
必要的同步。

这意味着:

  • 投机执行是不允许的,因为当线程按修改顺序访问一个特殊的输入,之后的读操作,必须由线程返回较新的值,并且之后的写操作必须发生在修改顺序之后。同样的,同一线程上允许读取对象的操作,要不就返回一个已写入的值,要不在对象的修改顺序后(也就是在读取后)再写入另一个值。虽然,所有线程都需要遵守程序中每个独立对象的修改顺序,但没有必要遵守在独立对象上的操作顺序。在5.3.3节中会有更多关于不同线程间操作顺序的内容。

所以,什么是原子操作?它如何来规定顺序?接下来的一节中,会揭晓答案。

5.2 C++中的原子操作和原子类型

原子操作是个不可分割的操作。系统的所有线程中,不可能观察到原子操作完成了一半;要么就是做了,要么就是没做,只有这两种可能。如果读取对象的加载操作是原子的,那么这个对象的所有修改操作也是原子的,所以加载操作得到的值要么是对象的初始值,要么是某次修改操作存入的值。

另一方面,非原子操作可能会被另一个线程观察到只完成一半。如果这个操作是一个存储操作,那么其他线程看到的值,可能既不是存储前的值,也不是存储的值,而是别的什么值。
如果非原子操作是一个加载操作,它可能先取到对象的一部分,然后值被另一个线程修改,然后它再取到剩余的部分,所以它取到的既不是第一个值,也不是第二个值,而是两个值的某种组合。如第3章所述,这就有了竞争风险,但在也就构成了数据竞争(见5.1节),会出现未定义行为。

C++中多数时候,需要原子类型对应得到原子的操作,我们先来看下这些类型。

5.2.1 标准原子类型

标准原子类型定义在头文件 中。这些类型的所有操作都是原子的,语言定义中只有这些类型的操作是原子的,不过可以用互斥锁来模拟原子操作。实际上,标准原子类型的实现就可能是这样模拟出来的:它们(几乎)都有一个 is_lock_free() 成员函数,这个函数可以让用户查询某原子类型的操作是直接用的原子指令( x.is_lock_free() 返回 true ),还是内部用了一个锁结构( x.is_lock_free() 返回 false )。

原子操作的关键就是使用一种同步操作方式,来替换使用互斥量的同步方式;如果操作内部使用互斥量实现,那么期望达到的性能提升就是不可能的事情。所以要对原子操作进行实现,最好使用用于获取且基于互斥量的实现来替代。这就是第7章所要讨论的无锁数据结构。

标准库提供了一组宏,在编译时对各种整型原子操作是否无锁进行判别。C++17中,所有原子类型有一个static constexpr成员变量,如果相应硬件上的原子类型X是无锁类型,那么X::is_always_lock_free将返回true。例如:给定目标硬件平台 std::atomic 可能是无锁的,所以 std::atomic::is_always_lock_free 将会返回true,不
过 std::atomic<uintmax_t> 可能只在最终运行的硬件上被支持时才没有锁,因为这是一个运行时属性,所以 std::atomic<uintmax_t>::is_always_lock_free 在该平台编译时可能为false。

宏都有ATOMIC_BOOL_LOCK_FREE , ATOMIC_CHAR_LOCK_FREE ,ATOMIC_CHAR16_T_LOCK_FREE , ATOMIC_CHAR32_T_LOCK_FREE ,ATOMIC_WCHAR_T_LOCK_FREE,ATOMIC_SHORT_LOCK_FREE ,
ATOMIC_INT_LOCK_FREE , ATOMIC_LONG_LOCK_FREE ,ATOMIC_LLONG_LOCK_FREE和ATOMIC_POINTER_LOCK_FREE。

它们指定了内置原子类型的无锁状态和无符号对应类型(LLONG对应long long,POINTER对应所有指针类型)。如
果原子类型不是无锁结构,那么值为0;如果原子类型是无锁结构,那么值为2;如果原子类
型的无锁状态在运行时才能确定,那么值为1。

只有 std::atomic_flag 类型不提供is_lock_free()。该类型是一个简单的布尔标志,并且在这种类型上的操作都是无锁的;当有一个简单无锁的布尔标志时,可以使用该类型实现一个简单的锁,并且可以实现其他基础原子类型。当觉得“真的很简单”时,就说明对 std::atomic_flag 明确初始化后,做查询和设置(使用test_and_set()成员函数),或清除(使
用clear()成员函数)都很容易。这就是:无赋值,无拷贝,没有测试和清除,没有任何多余操作。

剩下的原子类型都可以通过特化 std::atomic<> 类型模板得到,并且拥有更多的功能,但不可能都是无锁的(如之前解释的那样)。在主流平台上,原子变量都是无锁的内置类型(例如 std::atomic 和 std::atomic<void*> )。后面将会看到,每个特化接口所反映出的类型特点;位操作(如:&=)就没有为普通指针所定义,所以它也就不能为原子指针所定义。

除了直接使用 std::atomic<> 类型模板外,你可以使用在表5.1中所示的原子类型集。由于历史原因,原子类型已经添加入C++标准中,这些备选类型名可能参考相应的 std::atomic<> 特化类型,或是特化类型。同一程序中混合使用备选名与 std::atomic<> 特化类名,会使代码的移植性大打折扣。

表5.1 标准原子类型的备选名和与其相关的 std::atomic<> 特化类
在这里插入图片描述
C++标准库不仅提供基本原子类型,还定义了与原子类型对应的非原子类型,就如同标准库中的 std::size_t 。如表5.2所示这些类型:

表5.2 标准原子类型定义(typedefs)和对应的内置类型定义(typedefs)
在这里插入图片描述
好多种类型!不过,它们有一个相当简单的模式;对于标准类型进行typedef T,相关的原子类型就在原来的类型名前加上atomic_的前缀:atomic_T。除了singed类型的缩写是s,unsigned的缩写是u,和long long的缩写是llong之外,这种方式也同样适用于内置类型。对于 std::atomic 模板,使用相应的T类型去特化模板的方式,要好于使用别名的方式。

通常,标准原子类型是不能进行拷贝和赋值的,它们没有拷贝构造函数和拷贝赋值操作符。

但是,可以隐式转化成对应的内置类型,所以这些类型依旧支持赋值,可以使用load()和store()、exchange()、compare_exchange_weak()和compare_exchange_strong()。

它们都支持复合赋值符:+=, -=, *=, |= 等等。并且使用整型和指针的特化类型还支持++和–操作。当然,这些操作也有功能相同的成员函数所对应:fetch_add(), fetch_or()等等。赋值操作和成员函数的返回值,要么是被存储的值(赋值操作),要么是操作前的值(命名函数),这就能避免赋值操作符返回引用。为了获取存储在引用中的值,代码需要执行单独的读操作,从而允许另一个线程在赋值和读取的同时修改这个值,这也就为条件竞争打开了大门。

std::atomic<> 类模板不仅仅是一套可特化的类型,作为一个原发模板也可以使用用户定义类型创建对应的原子变量。因为,它是一个通用类模板,操作被限制为load(),store()(赋值和转换为用户类型),exchange(),compare_exchange_weak()和compare_exchange_strong()。

每种函数类型的操作都有一个内存排序参数,这个参数可以用来指定存储的顺序。5.3节中,会对存储顺序选项进行详述。现在,只需要知道操作分为三类:

  1. Store操作,可选如下顺序:memory_order_relaxed, memory_order_release,
    memory_order_seq_cst。
  2. Load操作,可选如下顺序:memory_order_relaxed, memory_order_consume,
    memory_order_acquire, memory_order_seq_cst。
  3. Read-modify-write(读-改-写)操作,可选如下顺序:memory_order_relaxed,
    memory_order_consume, memory_order_acquire, memory_order_release,
    memory_order_acq_rel, memory_order_seq_cst。

总结

本章中已经对C++内存模型的底层知识进行详尽的了解,并且了解了原子操作能在线程间提供同步。包含基本的原子类型,由 std::atomic<> 类模板和 std::experimental::atomic_shared_ptr<> 模板特化后提供的接口;以及对于这些类型的操作,还要有对内存序列选项的各种复杂细节,都由 std::atomic<> 类模板提供。

也了解了栅栏,如何让执行序列中,对原子类型的操作成对同步。最后,回顾了本章开始的一些例子,了解了原子操作也可以在不同线程上的非原子操作间使用,并进行有序执行。以及了解了高级工具所提供的同步能力。

下一章中,我们将看到如何使用高级同步工具,以及使用原子操作并发的设计访问高效的容器,我们还将编写一些并行处理数据的算法。


关于博主

wx/qq:binary-monster/1113673178
CSDN:https://blog.csdn.net/qq1113673178
码云:https://gitee.com/shiver
Github: https://github.com/ShiverZm
个人博客:www.shiver.fun

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

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

相关文章

【Docker】子系统与其相关名词的界定、Control Groups等详细讲解

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

回收旧物系统平台开发的功能

1、定位服务 为了方便用户寻找最近的废品回收点&#xff0c;小程序应该提供位置服务和导航功能。 2、垃圾分类知识普及 用户可以查看所有垃圾分类知识&#xff0c;每种物品属于哪一个类型的垃圾分类。一目了然。相当于一本活字典&#xff0c;用户可以随时翻看查阅垃圾分类的…

人工智能(pytorch)搭建模型11-pytorch搭建DCGAN模型,一种生成对抗网络GAN的变体实际应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能(pytorch)搭建模型11-pytorch搭建DCGAN模型&#xff0c;一种生成对抗网络GAN的变体实际应用&#xff0c;本文将具体介绍DCGAN模型的原理&#xff0c;并使用PyTorch搭建一个简单的DCGAN模型。我们将提供模型…

java+openlayer实现大气污染扩散模拟反演

一、模拟参数及效果 二、应用背景 大气污染是当今社会面临的一个重要问题。随着工业化和城市化的进程&#xff0c;大气污染问题变得越来越严重。为了更好地应对这个问题&#xff0c;许多科学家和研究人员开始探索大气污染扩散反演技术。 大气污染扩散反演技术是一种通过数学模…

给软件测试人的一封信,全网最佳“指路明灯“

一、一招鲜吃遍天下 你需要有一个核心技能。这个技能至少达到远超你的同事&#xff08;包括开发岗位的同事的&#xff09;平均水平。最好达到业界领先水平&#xff0c;且这个核心技能需要不断打磨提高。比如&#xff0c;我选择的核心技能是使用Python写代码。这个核心技能可以…

3.2 基于Java配置类整合SSM框架实现用户登录

一、基于Java配置类整合SSM框架实现用户登录 1、创建Maven项目 Maven项目 - SSMLoginNew 单击【Finish】按钮 2、添加相关依赖 在pom.xml文件里添加相关依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache…

Kubernetes 1.27 加快 Pod 启动速度

如何在大型集群中加快节点上的 Pod 启动&#xff1f;这是企业集群管理员常常会面临的问题。 这篇博文重点介绍了从 kubelet 一侧加快 Pod 启动的方法。此方法不涉及通过 kube-apiserver 由 controller-manager 创建 Pod 所用的时间段&#xff0c;也不包含 Pod 的调度时间或在其…

电脑最牛逼的截图方式

1.电脑桌面上空白的地方新建一个文本文档&#xff0c;将后缀名修改为bat&#xff0c;截图如下&#xff1a; 2.右键点击该文档编辑&#xff0c;在编辑界面输入start snippingtool&#xff0c;点击保存之后关闭该文档。 3.双击该文档&#xff0c;在模式里面选择响应的截图方式即可…

MySQL IDE与pymysql模块

一、IDE工具介绍 生产环境还是推荐使用mysql命令行&#xff0c;但为了方便我们测试&#xff0c;可以使用IDE工具 在此我们推荐使用Navicat软件或pycharm来连接数据库,这样就能更详细直观地查询数据 掌握&#xff1a; #1. 测试链接数据库 #2. 新建库 #3. 新建表&#xff0c;新增…

2023 年程序员高考试卷!你能答对几个?

又是一年高考季&#xff0c;一起来做做“程序员们的高考试卷”&#xff0c;压压惊吧~ 2023年普通高等学校招生全国统一考试 程序员的高考试卷&#xff08;A卷&#xff09; 考生类别&#xff1a;码农 1、程序员A&#xff1a;借我1000元吧。 程序员B&#xff1a;给你凑个整数…

Linux基础知识点2

Linux基础知识 适合有Linux基础的人群进行复习。 禁止转载&#xff01; 文件管理与常用命令 Linux的文件的组成部分&#xff1a; 文件名、inode(i节点)和block(真正存数据的区域)。 查看某个文件的属性&#xff1a; ls -lh #可看到有类似”-rw-r--r--”的属性符号 …

轻松来自实力,亚马逊云科技助力边界智能应对业务高峰值数据考验

边界智能&#xff08;Bianjie.AI&#xff09;是2016年创立于上海的国家高新技术企业和专精特新企业&#xff0c;同时也是以香港为全球总部、服务全球的区块链技术创新团队。公司专注于区块链技术支持的下一代互联网应用服务&#xff0c;自主研发了跨多条联盟链的分布式应用服务…

STL入门 + 刷题(下)

&#x1f442; Raindrops (Intl. Version) - Katja Krasavice/Leony - 单曲 - 网易云音乐 &#x1f442; Rush E (Playable Version) - Sheet Music Boss - 单曲 - 网易云音乐 &#x1f442; 最美的瞬间 - 真瑞 - 单曲 - 网易云音乐 &#x1f442; 你可别卷了 - SipSu小口酥…

CTF Crypto --- orz!

文章目录 题目解题过程 题目 from Crypto.Util.number import * from gmpy2 import *flag bxxx t len(flag)//3 part1 bytes_to_long(flag[:t]) part2 bytes_to_long(flag[t:2*t]) part3 bytes_to_long(flag[2*t:]) q getPrime(1024) p next_prime(q) n p * qo getPr…

面试必备,29个Java面试必考点、1000多道Java面试题

马上金九银十招聘旺季就到了&#xff0c;不知道大家是否准备好了&#xff0c;面对金九银十的招聘旺季&#xff0c;如果没有精心准备那笔者认为那是对自己不负责任&#xff1b;就我们Java程序员来说&#xff0c;多数的公司总体上面试都是以自我介绍项目介绍项目细节/难点提问基础…

文献下载神器:文献党下载器使用方法

文献党下载器是一款文献资源整合平台&#xff0c;把知网、万方、维普、超星/读秀、Web of Science、Elsevier&#xff08;ScienceDirect&#xff09;、Wiley 、SpringerLink、EI&#xff08;工程索引&#xff09;、IEEE&#xff08;电气电子工程师学会&#xff09;、Taylor &am…

ESP32-C3系列模组简介

ESP32-C3是一款安全稳定、低功耗、低成本的物联网芯片&#xff0c;搭载RISC-V 32位单核处理器&#xff0c;为物联网产品提供行业领先的射频性能、完善的安全机制和丰富的内存资源。 嵌入式智能终端、无线WIFI技术以及Internet的广泛应用必将使家居控制变得更加自动化、智能化和…

基础软件加速自主创新,openGauss成就业务“新箭头”

不久前&#xff0c;想必业界都注意到了MetaERP横空出世的消息。作为企业经营的核心系统&#xff0c;MetaERP突破外部封锁&#xff0c;实现完全自研替代&#xff0c;是华为有史以来牵涉面较广、复杂性较高的项目。这其实是国产基础软件迅速崛起的一个缩影。 基础软件产业是关系…

element-plus vue 错误汇总

input 无法输入0.01 element ui input 无法输入0.01 一输出0.0就报错&#xff0c;是因为写成了v-model.number&#xff0c;改成v-model即可。 <el-input v-model.number"formData.reduceMoney"class"input200"type"number"focus"discou…

C#开发的OpenRA游戏之建造物品的窗口1

C#开发的OpenRA游戏之建造物品的窗口1 前面已经分析了基地工程车的创建和移动,当玩家把基地工程车移动到合适的位置,就会进行部署基地,也即是选择一个离矿场比较近的位置,因为这样做可以提高采矿的速度,减少采矿车的运输时间。 接着下来,虽然基地是建立了,但是还需要创…