C++11中的内存模型

news2024/9/28 5:28:09

一、几种关系术语

1.1、sequenced-before

sequenced-before用于表示同一个线程中,两个操作上的先后顺序,这个顺序是非对称、可以进行传递的关系。

它不仅仅表示两个操作之间的先后顺序,还表示了操作结果之间的可见性关系。两个操作A和操作B,如果有A sequenced-before B,除了表示操作A的顺序在B之前,还表示了操作A的结果操作B可见

1.2、happens-before

与sequenced-before不同的是,happens-before它既包括同一个线程里的操作顺序,也包不同线程操作间的顺序,同样的也是非对称、可传递的关系。

如果A happens-before B,则A的内存状态将在B操作执行之前就可见

1.3、synchronizes-with

synchronizes-with 描述的是两个线程对同一个原子变量的修改和读取之间的关系,如果一个线程修改某变量的之后的结果能被其它线程可见,那么就是满足synchronizes-with关系的。

显然,满足synchronizes-with关系的操作一定满足happens-before关系了。

二、C++11中支持的内存模型

从C++11开始,就支持以下几种内存模型:

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

与内存模型相关的枚举类型有以上六种,但是其实分为四类,如下图所示,其中对一致性的要求逐渐减弱,以下来分别讲解。
在这里插入图片描述

在原子操作上添加六种内存顺序标记(中的一部分),会影响(但不一定改变;视 CPU 架构)原子操作附近的内存访问顺序(包括其他原子操作,亦包含对非原子变量的读写操作)。注意,内存顺序(通过六种标记)讨论的实际上是线程内原子操作附近非原子操作访问内存的顺序,而非是多线程之间的执行顺序。只不过,因为原子变量自身可能建立了线程间的同步关系,所以两个线程内各自的内存顺序会经由原子变量的同步建立间接的顺序关系。亦即,内存顺序本质上是在讨论单线程内指令执行顺序对多线程影响的问题

2.1、顺序一致顺序 (sequentially-consistent ordering)

memory_order_seq_cst 为参数对原子变量进行load、store或者read-modify-write操作。这是默认的内存模型,c++11所有的原子操作都是采用memory_order_seq_cst作为默认参数。所有以memory_order_seq_cst为参数的原子操作(不限于同一个原子变量),对所有线程来说有一个全局顺序(total order),并且两个相邻memory_order_seq_cst原子操作之间的其它操作(包括非原子变量操作),不能reorder到这两个相邻操作之外。【注:同一个程序的不同运行,这个全局顺序是可以不一样的】。

我们通过下面的例子来理解一下顺序一致性:

std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};

// 线程A
void write_x() {
    x.store(true, memory_order_seq_cst);
}

// 线程B
void write_y() {
    y.store(true, memory_order_seq_cst);
}

// 线程C
void read_x_then_y() {
    while (!x.load(memory_order_seq_cst))
        ;
    if (y.load(memory_order_seq_cst)) {
        ++z;
    }
}

// 线程D
void read_y_then_x() {
    while (!y.load(memory_order_seq_cst))
        ;
    if (x.load(memory_order_seq_cst)) {
        ++z;
    }
}

int main() {
    std::thread thread_a(write_x);
    std::thread thread_b(write_y);
    std::thread thread_c(read_x_then_y);
    std::thread thread_d(read_y_then_x);

    thread_a.join(); thread_b.join(); thread_c.join(); thread_d.join();
    assert(z.load() != 0);  // 一定不会失败
}

4个线程对两个原子变量x和y的操作都是memory_order_seq_cst,构成了顺序一致性(sequentially-consistent),因此对4个线程来说,对原子变量x和y的操作顺序是一致的,在这个全局一致的操作顺序里:

(Ⅰ)要么x.store(true, memory_order_seq_cst)发生在y.store(true, memory_order_seq_cst)之前,这时候线程D的x.load(memory_order_seq_cst)一定读到true,从而执行z++ ;

(Ⅱ)要么y.store(true, memory_order_seq_cst)发生在x.store(true, memory_order_seq_cst)之前,这时候线程C的y.load(memory_order_seq_cst)一定读到true,从而执行z++ ;

不论哪种情况,z值都会被修改,因此main()函数最后的assert语句一定不会失败。

2.2、获取-释放顺序 (acquire-release ordering)

  • memory_order_acquire:用来修饰一个读操作,表示在本线程中,所有后续的关于此变量的内存操作都必须在本条原子操作完成后执行。
  • memory_order_release:用来修饰一个写操作,表示在本线程中,所有之前的针对该变量的内存操作完成后才能执行本条原子操作
  • memory_order_acq_rel:同时包含memory_order_acquire和memory_order_release标志
struct Point {
  int x_;
  int y_;
};

Point g_point;
std::atomic<int> g_guard(0);

// 线程A
void writePoint() {
    g_point.x_ = 1;
    g_point.y_ = 2;
    
    // 以memory_order_release写入1到原子变量
    g_guard.store(1, memory_order_release);
}

// 线程B
void readPoint() {
    // 以memory_order_acquire读取原子变量,直到读到1 (线程A所写入的值)
    while (g_guard.load(memory_order_acquire) != 1) {
      this_thread::yield();
    }
    
    // while 循环结束时,一定是以memory_order_acquire读到线程A写入的值1
    assert(g_point.x_ == 1 && g_point.y_ == 2); // 不会失败
}

这个例子中的g_guard读写就是synchronize-with 关系, 这里synchronize-with 描述的是两个线程对同一个原子变量的修改和读取之间的关系,它在两个相关线程建提供了一个比较强的memory order约束: 线程A的store操作之前的所有内存修改,对线程B的load之后的操作都可见,并且线程A的store操作之前的指令不允许reorder到store操作之后,线程B的load操作之后的指令不允许reorder到load之前synchronize-with 关系像是在两个线程之间建立了一个内存屏障,这个屏障引入了一种较强的先后顺序,屏障前的内存修改对屏障后的所有操作都可见。

具体到上面这个例子,就是线程A对结构体g_point的修改,在线程B的“while循环最后一次g_guard.load(memory_order_acquire)”之后都是可见的,因此线程B的assert不会失败:
在这里插入图片描述

2.3、释放-消费顺序 (Release-Consume ordering)

一个线程以memory_order_release对原子变量进行store操作,另一个线程以memory_order_consume对同一个原子变量进行load操作,从上面对Acquire-Release模型的分析可以知道,虽然可以使用这个模型做到两个线程之间某些操作的synchronizes-with关系,然后这个粒度有些过于大了。

在很多时候,线程间只想针对有依赖关系的操作进行同步,除此之外线程中的其他操作顺序如何无所谓。比如下面的代码中:

b = *a; 
c = *b;

其中第二行代码的执行结果依赖于第一行代码的执行结果,此时称这两行代码之间的关系为“carry-a-dependency ”。C++中引入的memory_order_consume内存模型就针对这类代码间有明确的依赖关系的语句限制其先后顺序。

来看下面的示例代码:

#include <string>
#include <thread>
#include <atomic>
#include <assert.h>
struct X
{
    int i;
    std::string s;
};

std::atomic<X*> p;
std::atomic<int> a;

void create_x()
{
    X* x=new X;
    x->i=42;
    x->s="hello";
    a.store(99,std::memory_order_relaxed);
    p.store(x,std::memory_order_release);
}

void use_x()
{
    X* x;
    while((x = p.load(std::memory_order_consume)) != nullptr)
        std::this_thread::sleep_for(std::chrono::microseconds(1));
    assert(x->i==42);
    assert(x->s=="hello");
    assert(a.load(std::memory_order_relaxed)==99);
}
int main()
{
    std::thread t1(create_x);
    std::thread t2(use_x);
    t1.join();
    t2.join();
}

以上的代码中:

  • create_x线程中的store(x)操作使用memory_order_release,而在use_x线程中,有针对x的使用memory_order_consume内存模型的load操作,两者之间由于有carry-a-dependency关系,因此能保证两者的先后执行顺序。所以,x->i == 42以及x->s==“hello"这两个断言都不会失败。
  • 然而,create_x中针对变量a的使用relax内存模型的store操作,use_x线程中也有针对变量a的使用relax内存模型的load操作。这两者的先后执行顺序,并不受前面的memory_order_consume内存模型影响,所以并不能保证前后顺序,因此断言a.load(std::memory_order_relaxed)==99真假都有可能。

以上可以对比Acquire-Release以及Release-Consume两个内存模型,可以知道:

  • Acquire-Release能保证不同线程之间的Synchronizes-With关系,这同时也约束到同一个线程中前后语句的执行顺序。
  • 而Release-Consume只约束有明确的carry-a-dependency关系的语句的执行顺序,同一个线程中的其他语句的执行先后顺序并不受这个内存模型的影响。

2.4、宽松顺序 (relaxed ordering)

以memory_order_relaxed作为参数的原子操作,这种类型对应的松散内存模型,这个对于操作顺序没有任何约束,只保证操作的原子性

atomic<int> x = {0};
atomic<int> y = {0};

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B

// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

可能产生r1 = r2 = 42 的结果,尽管在线程1里 A sequenced-before B, 在线程2里 C sequenced-before D,但是因为memory_order_relaxed不提供任何顺序约束,对于线程1来说,可能是D发生在C的前面,整个执行顺序可能是D ==> A ==> B ==> C,从而导致r1 = r2 = 42

三、与 volatile 的关系

voldatile关键字首先具有“易变性”,声明为volatile变量编译器会强制要求读内存,相关语句不会直接使用上一条语句对应的的寄存器内容,而是重新从内存中读取。

其次具有”不可优化”性,volatile告诉编译器,不要对这个变量进行各种激进的优化,甚至将变量直接消除,保证代码中的指令一定会被执行。

最后具有“顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。不过要注意与非volatile变量之间的操作,还是可能被编译器重排序的。

需要注意的是其含义跟原子操作无关,比如:volatile int a; a++; 其中a++操作实际对应三条汇编指令实现”读-改-写“操作(RMW),并非原子的。

3.1、区别

上面内存模型操作都是原子的,而volatile不是原子的。

参考文档

https://www.codedump.info/post/20191214-cxx11-memory-model-2/

https://liam.page/2021/12/11/memory-order-cpp-02/

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

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

相关文章

《长安的荔枝》阅读笔记

《长安的荔枝》阅读笔记 2023年6月9号在杭州的小屋读完&#xff0c;作者以“一骑红尘妃子笑”的典故&#xff0c;想象拓展出来的荔枝使李善德&#xff0c;为了皇帝要求在贵妃寿辰&#xff0c;六月一号那天要吃到10斤的荔枝。需要从广州运送到长安即如今的西安。本来以为这个差事…

SequenceDiagram 查看代码时序图的利器,做技术方案必备!

前言 “ 无论是快速了解业务流程&#xff0c;还是快速的熟悉系统的业务代码逻辑&#xff0c;以及各个类和方法等的调用关系&#xff0c;时序图无疑是其中一种不可获取的简便快捷的方式。一起来了解下&#xff0c;IDEA如何快速生成时序图吧。” 工作中&#xff0c;经常需要绘制…

async 和 await的用法

async 函数 async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction构造函数的实例&#xff0c;并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promis的异步行为&#xff0c;而无需刻意地链式调用 promise。 asyn…

MySQL实用命令

一、 DISTINCT 去重 案例&#xff1a;user_test表对班级字段进行去重操作 SELECT DISTINCT class FROM user_test 二、 的作用 案例&#xff1a; SELECT NULL10 三、 concat 实现连接字符串 ​​​案例&#xff1a; SELECT CONCAT(NAME,class) AS 姓名班级 FROM user_test 四…

看了那场直播后,我们发起了一个讨论

几天前&#xff0c;我们做了一场直播&#xff0c;关于如何用 Serverless 技术让文化古籍“活过来”。 完整视频进入阿里云云原生视频号看直播回放 背景信息&#xff1a; 通过阿里云函数计算帮助复旦大学特藏中心建立数字图书馆&#xff0c;为用户提供更丰富、更具互动性的古籍浏…

使用Claude为杜甫写简历

杜甫生平的百度链接&#xff1a;杜甫&#xff08;唐代著名现实主义诗人&#xff09;_百度百科 选中所有内容。 打开claude网页&#xff1a;App unavailable \ Anthropic 粘贴刚刚复制的内容。 输入&#xff1a; 按照这个格式总结杜甫的一生&#xff1a;“公元712年 杜甫出…

Shell脚本学习-while循环2

案例&#xff1a;使用while守护进程的方式监控网站&#xff0c;每隔10秒确定一次网站是否正常。 [rootvm1 scripts]# cat url_monitor1.sh #!/bin/bashif [ $# -ne 1 ]thenecho "USAGE: $0 url"exit 1 fiwhile true doif [ curl -I -s -w "%{http_code}" -…

友盟+、GrowingIO和神策数据 对比

对于市面上的数据平台&#xff0c;先简单归个类。 1、移动统计平台&#xff0c;如友盟、talkingdata、百度云统计、腾讯移动应用统计等。 相同点是数据源都是埋点数据。友盟有免费版本。 前端效果&#xff1a;展现形式上为BI报表。 常用操作是页面内点击和筛选。 使用要求…

使用 RSA 密钥进行 SSH 连接

使用 RSA 密钥进行 SSH 连接 平时用 SSH 连树莓派和虚拟机每次都要输入密码&#xff0c;比较烦人。Windows Terminal 出于安全原因是不支持记录密码进行自动连接的功能的&#xff0c;所以还是老老实实创建 RSA 密钥进行连接好了。 生成 RSA 密钥对 Windows 上可以用 Putty K…

leetcode17. 电话号码的字母组合(java)

电话号码的字母组合 leetcode17. 电话号码的字母组合题目描述回溯算法代码演示 回溯算法 leetcode17. 电话号码的字母组合 难度 中等 leetcode17 跳转链接 题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数…

基于遗传算法的试题组卷(二)

实例讲解 一、准备工作 1、问题实体 问题实体包含编号、类型&#xff08;类型即题型&#xff0c;分为五种&#xff1a;单选&#xff0c;多选&#xff0c;判断&#xff0c;填空&#xff0c;问答&#xff0c; 分别用1、2、3、4、5表示&#xff09;、分数、难度系数、知识点。一…

前端Vue入门-day08-vant组件库

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 vant 组件库 安装 导入 全部导入 按需导入 浏览器配饰 Viewport 布局 Rem 布局适配 vant 组件库 …

九耶|阁瑞钛伦特:产品经理面试题—产品经理在工作中是如何划分需求优先级的?

产品经理在工作中划分需求优先级是为了指导产品团队的开发和发布流程。以下是产品经理在划分需求优先级时通常考虑的因素&#xff1a; 业务目标&#xff1a;产品经理会与企业领导层或业务方合作&#xff0c;了解公司的战略目标和销售策略。然后&#xff0c;他们会根据这些目标评…

【数据分享】1901-2022年1km分辨率的逐年平均气温栅格数据(免费获取)

气温数据是我们最常用的气象指标之一&#xff0c;之前我们给大家分享过1901-2022年1km分辨率逐月平均气温栅格数据&#xff0c;该数据来源于国家青藏高原科学数据中心&#xff0c;数据持续更新&#xff0c;最新数据为2022年的数据&#xff01;很多小伙伴拿到数据后还咨询我们有…

Docker-Compose编排与部署(lnmp实例)

第四阶段 时 间&#xff1a;2023年8月3日 参加人&#xff1a;全班人员 内 容&#xff1a; Docker-Compose编排与部署 目录 一、Docker Compose &#xff08;一&#xff09;概述 &#xff08;二&#xff09;Compose适用于所有环境&#xff1a; &#xff08;三&#xf…

数据结构基础4:带头指针双向。

一.基本概念&#xff1a; 一.基本结构&#xff1a; 1.逻辑结构&#xff1a; 二.结构体的基本组成。 和单链表非常大的区别就是函数不需要传二级指针&#xff0c;因为头不存有效数据。头节点不需要改变。 二.功能接口的实现 0.创建一个新的节点 //创建新的节点ListNode* buy…

【C++】BSTree 模拟笔记

文章目录 概念插入和删除非递归实现中的问题递归中的引用简化相关OJ复习直达 概念 由下面二叉搜索树的性质可以知道&#xff0c;中序遍历它便可以得到一个升序序列&#xff0c;查找效率高&#xff0c;小于往左找&#xff0c;大于往右走。最多查找高度次&#xff0c;走到到空&am…

黑马头条 各种踩坑合集 从0到1 欢迎留言提问

《黑马头条》SpringBootSpringCloud Nacos等企业级微服务架构项目_黑马头条项目_软工菜鸡的博客-CSDN博客 Day01 用他的 centos镜像 自动集成了 nacos 所有的配置 大部分都要改mysql密码啥的&#xff1b; 启动他的项目&#xff0c;有些时候要启动 redis 容器&#xff1b;镜像里…

超详细|ChatGPT论文润色教程

本文讲述使用中科大开源ChatGPT论文辅助工具&#xff0c;对论文进行润色 祝看到本教程的小伙伴们都完成论文&#xff0c;顺利毕业。 可以加QQ群交流&#xff0c;一群&#xff1a; 123589938 第一章 介绍 今天给大家分享一款非常不错的ChatGPT论文辅助工具&#xff0c;使用了专…

谁是全球最小ARM MCU?

先是从百度上查找的&#xff0c;找了半天&#xff0c;也找到了一部分。 大小基本在一二毫米左右&#xff0c;外设有多有少&#xff0c;但是好多都买不到货。 而且有些特别小众&#xff0c;开发环境压根没接触过。 然后去外面找了一下&#xff0c;竟然有好几个提到了HC32L110&am…