广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >浅析C++ atomic 和 memory ordering
  • 299
分享到

浅析C++ atomic 和 memory ordering

2024-04-02 19:04:59 299人浏览 独家记忆
摘要

如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果

如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:

  • 即使是简单的语句,c++ 也不保证是原子操作。
  • CPU 可能会调整指令的执行顺序。
  • 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。

利用 C++ 的 atomic<T> 能完成对象的原子的读、写以及RMW(read-modify-write),而参数 std::memory_order 规定了如何围绕原子对象的操作进行排序memory order 内存操作顺序其实是 内存一致性模型 (Memory Consistency Model),解决处理器的 write 操作什么时候能够影响到其他处理器,或者说解决其他处理处理器什么时候能够观测到当且 写CPU/写线程 写入内存的值,有了 memory odering,我们就能知道其他处理器是怎么观测到 store 指令的影响的。

一致模型有很多种,在 Wikipedia 里面搜索 Consistency model 即可看到,目前 C++ 所用到有 Sequential Consistency 和 Relaxed Consistency 以及 Release consistency。

Memory Operation Ordering

我们所编写的程序会定义一系列的 loadstore 操作,也就是 Program ordering,这些 load 和 store 的操作应用在内存上就有了内存操作序(memory operation ordering),一共有四种内存操作顺序的限制,不同的内存一致模型需要保持不同级别的操作限制,其中 W 代表写,R 代表读:

  • W -> R:写入内存地址 X 的操作必须比在后面的程序定义序列的读取地址 Y 之前提交 (commit), 以至于当读取内存地址 Y 的时候,写入地址 X 的影响已经能够在读取Y时被观测到。
  • R -> R: 读取内存地址 X 的操作必须在后序序列中的读取内存地址 Y 的操作之前提交。
  • R -> W:读取内存地址 X 的操作必须在后序序列中读取内存地址 Y 的操作之前提交。
  • W -> W:写入内存地址 X 的操作必须在后续序列中写入内存地址 Y 的操作之前提交。

提交的意思可以理解为,后面的操作需要等前面的操作完全执行完才能进行下一个操作。

Sequential consistency

序列一致是 Leslie Lamport 提出来的,如果熟悉分布式共识算法 Paxos ,那么应该不陌生这位大科学家,而序列一致的定义是:

  • the result of any execution is the same as-if (任何一种执行结果都是相同的就好像)
  • the operations of all threads are executed in some sequential order (所有线程的操作都在某种次序下执行)the operations of each thread appear in this sequence in the order specified by their program (在全局序列中的,各个线程内的操作顺序由程序指定的一致)

组合起来:全局序列中的操作序列要和线程所指定的操作顺序要对应,最终的结果是所有线程指定顺序操作的排列,不能出现和程序指定顺序组合不出来的结果。

怎么做会违反 sequcential consistency(SC)?也就是 SC 的反例是什么?

  • 乱序执行 (out-of-order)
  • 内存访问重叠,写A的过程中读取A,宽于计算机Word的,64位机器写128位变量

更加形象的理解可以从内存的角度来看:

所有的处理器都按照 program order 发射 loadstore 的操作,而内存一个地一个地从上面 4 个处理器中读取指令,并且仅当完成一个操作后才会去执行下一个操作,类似于多个 producer 一个 consumer 的情况。

(Lamport 一句话,让我为他理解了一下午)

SC 需要保持所有的内存操作序(memory operation ordering),也是最严格的一种,并且 SC 是 c++ atomic<T> 默认的以一种内存模型,对应 std::memory_order_seq_cst,可以看到标准库中的函数定义将其设置为了默认值:

bool
    load(memory_order __m = memory_order_seq_cst) const noexcept
    { return _M_base.load(__m); }

Relaxed Consistency

松弛内存序,对应的 std::memory_order_relaxed,在 cppreference 上的说明是:"不保证同步操作,不会将一定的顺序强加到并发内存访问上,只保证原子性和修改顺序一致性",并且通常用于计数器,比如 shared_ptr 的引用计数。

松弛内存序不再保证 W -> R,不相互依赖的读写操作可以在 write 之前或者在同一时间段并行处理。(读内存并不是想象中的那么简单,有内存寻址过程,将内存数据映射到 cache block,发送不合法位用于缓存替换)

好处是什么?性能,执行命令的写操作的延迟都被抹去了,cpu 能够更快的执行完一段带有读写的指令序列。

具体实现是通过在 cpu 和 cache 之间加入一个 write buffer,如下图:

处理器 Write 命令将会发送到 Write Buffer,而 Read 命令就直接能访问 cache,这样可以省去写操作的延迟。Write Buffer 还有一个细节问题,放开 W -> R 的限制是当 WriteRead 操作内存地址不是同一个的时候,R/W 才能同时进行甚至 R 能提前到 W 之前,但如果 Write Buffer 中有一个 Read 所依赖的内存地址就存在问题,Read 需要等在 Write buffer 中的 Write 执行完成才能继续吗?只需要 Read 能直接访问这个 Write Buffer,如下(注:这里的Load通常和Read等意,StoreWrite等意):

Release Consistency

在这种一致性下,所有的 memory operation ordering 都将不再维护,是最激进的一种内存一致模型,进入临界区叫做 Acquire ,离开临界区叫做 Release。所有的 memory operation ordering 都将不再维护,处理器支持特殊的同步操作,所有的内存访问指令必须在 fence 指令发送之前完成,在 fench 命令完成之前,其他所有的命令都不能开始执行。

Intel x86/x64 芯片在硬件层面提供了 total store ordering 的能力,如果软件要求更高级别的一致性模型,处理器提供了三种指令:

  • mm_lfence:load fence,等待所有 load 完成
  • mm_sfence:store fence,等待所有 store 完成
  • mm_mfence:完全读写屏障

而在 ARM 架构上,提供的是一种非常松弛(very relaxed)内存一致模型。

PS. 曾经有个公司做出了支持 Sequential Consistency 的硬件,但是最终还是败给了市场。

Acquire/Release

Acquire/release 对应 std::memory_order_acquirestd::memory_order_acquire,它们的语义解释如下:

  • Acquire:如果一个操作 X 带有 acquire 语义,那么在操作 X 后的所有 load/store 指令都不会被重排序到操作 X 之前,其他处理器会在看到操作X后序操作的影响之前看到操作 X 的影响,也就是必须先看到 X 的影响,再是后续操作的影响。
  • Relase:如果一个操作 X 带有 release 语义,那么在操作 X 之前的所有 load/store 指令操作都不会被重排序到操作 X 之后,其他处理器会先看到操作 X 之前的操作。

Acquire/Release 常用在互斥(mutex lock)和自旋锁(spin lock),获得一个锁和释放一个锁需要分别使用 Acquire 和 Release 语义防止指令操作被重排出临界区,从而造成数据竞争。

Acquire/Consume

Acquire/Consume 对应 std::memory_order_acquirestd::memory_order_consume,两种内存模型的组合仅有 consume 不同于 release,不同点在于,假设原子操作 X, Release 会防止 X 之前的所有指令不会被重排到 X 之后,而 Consume 只能保证依赖的变量不会被重排到 X 之后,引入了依赖关系。

但是在 cppreference 上面写着,“释放消费顺序的规范正在修订中,而且暂时不鼓励使用memory_order_consume。”,所以暂时不对其做深入的研究。

Volatile

volatile 关键词通常会被拿出来说,因为通常会在并发编程中被错误使用:

volatile 的翻译是“不稳定的,易发生变化的”,编译器会始终读取 volatile 修饰的变量,不会将变量的值优化掉,但是这不是用在线程同步的工具,而是一种错误行为,cppreference上面写道:“volatile 访问不建立线程间同步,volatile 访问不是原子的,且不排序内存,非 volatile 内存访问可以自由地重排到 volatile 访问前后。”(Visual Studio 是个例外)。

volatile 变量的作用是用在非常规内存上的内存操作,常规内存在处理器不去操作的时候是不会发生变化的,但是像非常规内存如内存映射I/O的内存,实际上是在和外围设备做串口通信,所以不能省去。(《modern effective c++》)

到此这篇关于C++ atomic 和 memory ordering的文章就介绍到这了,更多相关C++ atomic 和 memory ordering内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 浅析C++ atomic 和 memory ordering

本文链接: https://www.lsjlt.com/news/147376.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 浅析C++ atomic 和 memory ordering
    如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果...
    99+
    2022-11-13
  • c++ atomic原子编程中Memory Order的示例分析
    这篇文章给大家分享的是有关c++ atomic原子编程中Memory Order的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。概述但是,基于内核对象的同步,会带来昂贵的上下文切换(用户态切换到内核态,占...
    99+
    2023-06-15
  • 浅析C++函数模板和类模板
    目录一、函数模板1、函数模板的定义和使用2、函数模板的编译原理3、函数模板的声明二、类模板1、类模板的定义和使用2、类模板的编译原理3、类模板的继承和派生C++语言全盘继承了C语言的...
    99+
    2022-11-13
  • 浅析C++浅拷贝与深拷贝的联系和区别
    文章简述 c++中构造函数分为三类:无参构造、带参构造和拷贝构造,其中拷贝构造可分为默认拷贝(浅拷贝)、深拷贝,在程序中,这里我们主要讲浅拷贝和深拷贝的联系和区别。 首先,我们要明白...
    99+
    2022-11-13
  • 浅析C++中的重载,隐藏和覆盖
    重载关系 一组函数要重载,必须处在同一个作用域中 ,而且函数名字相同,参数列表不同 代码1中的Base中的 show() 和show(int) 属于重载 代码2中的Base中的 sh...
    99+
    2022-12-08
    C++重载 隐藏 覆盖 C++重载 C++ 隐藏 C++ 覆盖
  • 如何浅析C#打印和C#打印预览的实现
    如何浅析C#打印和C#打印预览的实现,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。我们谈到C#打印和C#打印预览的实现其中主要就是包括:页面设置、打印预览、打印...
    99+
    2023-06-17
  • 深入浅析C# 11 对 ref 和 struct 的改进
    目录前言背景ref 字段生命周期scopedunscopedref struct 约束反射实际用例栈上定长列表栈上链表未来计划高级生命周期总结前言 C# 11 中即将到来一个可以让重...
    99+
    2022-11-13
  • 浅析C语言初阶的常量和变量
    目录什么是常量和变量常量示例定义变量的方法变量的分类示例变量的使用变量的作用域和生命周期作用域生命周期结语什么是常量和变量 在C程序执行过程中,其值不发生改变的量称为常量,其值可变的...
    99+
    2023-05-19
    C++常量和变量 C++常量 C++变量
  • 浅析C++中dynamic_cast和static_cast实例语法详解
    目录1. static_cast1.2 为什么要有static_cast等1.2 static_cast的作用1.3 static_cast用法2. dynamic_cast2.1 ...
    99+
    2022-11-12
  • c语言和c++语言中const修饰的变量区别浅析
    目录c:修饰全局变量:修饰局部变量:c++:修饰全局变量:修饰局部变量:总结:在c语言中:在c++语言中:总结c: 修饰全局变量: 用const修饰的全局变量是没有办法直接修改的,间...
    99+
    2022-11-13
  • 浅析Python与Java和C之间有哪些细微区别
    目录1.变量是什么(1)C认为变量应该是容器(2)Python认为变量应该是标签(3)Java认为我们不应该关心这个问题2.循环与迭代(1)传统的循环(2)继承与发展(3)新的问题3...
    99+
    2022-11-11
  • 浅析string类字符串和C风格字符串之间的区别
    最大的区别就是C风格的字符串是静态的,不可以动态变化,使用极为麻烦。而C++的std::string类型动态管理,非常方便。 C风格字符串和char数组是不一样的,看下面两种定义:c...
    99+
    2022-11-15
    string 类字符串 风格字符串
  • 浅析C++模板类型中的原样转发和可变参数的实现
    目录原样转发的意义模板的可变参数总结原样转发的意义 前文我们实现了一个my_move函数,用来模拟stl的move操作,实现去引用的功能。其内部的原理就是通过remove_refer...
    99+
    2022-11-13
    C++ 原样转发 C++ 可变参数
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作