C++六种内存序详解

这篇具有很好参考价值的文章主要介绍了C++六种内存序详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

要理解C++的六种内存序,我们首先须要明白一点,处理器读取一个数据时,可能从内存中读取,也可能从缓存中读取,还可能从寄存器读取。对于一个写操作,要考虑这个操作的结果传播到其他处理器的速度。
并且,编译器的指令重排和CPU处理器的乱序执行也是我们需要考虑的因素。

C++六种内存序详解

 

我们先看一个具体的例子,下图中P1和P2指代不同的processor,假设P2缓存了Data的值

C++六种内存序详解

  1. P1 先完成了 Data 在内存上的写操作, Data=2000;
  2. P1 没有等待 Data 的写结果传播到 P2 的缓存中,继续进行 Head 的写操作, Head=1;
  3. P2 读取到了内存中 Head 的新值;
  4. P2 继续执行,读到了缓存中 Data 的旧值, Data=0。

在讲六种内存序之前,先明白两种关系

Happens-before

happens-before 关系是一种逻辑关系,它确保内存操作的有序性和可见性。如果在程序中操作A happens-before操作B,那么操作A的效果(包括对内存的修改)保证对启动操作B的线程可见,并且A的执行在时间上先于B。

Synchronizes-with

synchronizes-with 是C++内存模型中的一个特定类型的 happens-before 关系,它主要用于描述同步机制(如互斥锁、原子操作等)。这种关系指明了程序中的两个操作之间通过某种同步机制直接建立的顺序关系。如果操作A synchronizes-with操作B,则A happens-before B。

例如:

  • 一个线程释放(unlock)一个互斥锁,然后另一个线程获取(lock)这个互斥锁,释放操作与获取操作之间存在synchronizes-with关系。
  • 对原子变量执行store操作(使用memory_order_release或更强的顺序)与另一个线程上对该原子变量进行load操作(使用memory_order_acquire或更强的顺序)之间也存在synchronizes-with关系。

memory_order_relaxed

唯一的要求是在同一线程中,对同一原子变量的访问不可以被重排,不同的原子变量的操作顺序是可以重排的。它不提供任何跨线程的内存顺序保证。

1 int x = 0;
2 int y = 0;
3 // Thread 1:
4 r1 = y.load(std::memory_order_relaxed); // A
5 x.store(r1, std::memory_order_relaxed); // B
6 // Thread 2:
7 r2 = x.load(std::memory_order_relaxed); // C 
8 y.store(42, std::memory_order_relaxed); // D

代码执行后,y1=y2=42的情况是可能出现的,因为第8行的代码可以被重排到第7行之前执行。

memory_order_release & memory_order_acquire & memory_order_consume

Acquire-Release能保证不同线程之间的Synchronizes-With关系,这同时也约束到同一个线程中前后语句的执行顺序。release语句之前的所有变量的读写操作(including non-atomic and relaxed atomic)都对另一个线程中的acquire之后的代码可见。

 1 #include <atomic>
 2 #include <thread>
 3 #include <assert.h>
 4 
 5 std::atomic<bool> x,y;
 6 std::atomic<int> z;
 7 
 8 void write_x_then_y()
 9 {
10     x.store(true,std::memory_order_relaxed);// 1
11     y.store(true,std::memory_order_release);// 2
12 }
13 
14 void read_y_then_x()
15 {
16     while(!y.load(std::memory_order_acquire));// 3
17     if(x.load(std::memory_order_relaxed)) //4
18         ++z;
19 }
20 
21 int main()
22 {
23     x=false;
24     y=false;
25     z=0;
26     std::thread a(write_x_then_y);
27     std::thread b(read_y_then_x);
28     a.join();
29     b.join();
30     assert(z.load()!=0);
31 }

代码执行后assert永远为true. 代码中1一定发生在2之前(happens-before), 2一定发生在3之前(Synchronizes-With), 3一定发生在4之前(happens-before);

而Release-Consume只约束有明确的carry-a-dependency关系的语句的执行顺序,同一个线程中的其他语句的执行先后顺序并不受这个内存模型的影响。release语句之前的有依赖关系的变量的读写操作都对另一个线程中的consume之后的代码可见。

上面的代码的第16行如果从std::memory_order_acquire改成std::memory_order_consume, 最后z是有可能为0的,因为变量x和y之间不存在依赖关系,thread b不一定能看到thread a中的对x的写操作。

根据cppreference,目前memory_order_consume是不建议使用的。

C++六种内存序详解

memory_order_acq_rel

它结合了memory_order_acquire 和 memory_order_release 的特性,确保了本线程原子操作的读取时能看到其他线程的写入(acquire 语义),并且本线程的写入对其他线程可见(release 语义),主要用于read-modify-write操作,如fetch_sub/add或compare_exchange_strong/weak。

 1 #include <atomic>
 2 #include <iostream>
 3 #include <thread>
 4 
 5 struct Node {
 6     int data;
 7     Node* next;
 8 };
 9 
10 std::atomic<Node*> head{nullptr};
11 
12 void append(int value) {
13     Node* new_node = new Node{value, nullptr};
14     
15     // 使用 memory_order_acq_rel 来确保对 head 的修改对其他线程可见, 同时确保看到其他线程对 head 的修改
16     Node* old_head = head.exchange(new_node, std::memory_order_acq_rel);
17     
18     new_node->next = old_head;
19 }
20 
21 void print_list() {
22     Node* current = head.load(std::memory_order_acquire);
23     while (current != nullptr) {
24         std::cout << current->data << " ";
25         current = current->next;
26     }
27     std::cout << std::endl;
28 }
29 
30 void thread_func() {
31     for (int i = 0; i < 10; ++i) {
32         append(i);
33     }
34 }
35 
36 int main() {
37     std::thread t1(thread_func);
38     std::thread t2(thread_func);
39     
40     t1.join();
41     t2.join();
42     
43     print_list();
44     
45     return 0;
46 }

memory_order_seq_cst

它满足memory_order_acq_rel的所有特性,除此之外,它强制受影响的内存访问传播到每个CPU核心。它不仅保证了单个原子变量操作的全局顺序,而且保证了所有使用顺序一致性内存序的原子变量之间的操作顺序在所有线程的观测中是一致的。

 我们先看一个例子

 1 #include <atomic>
 2 #include <cassert>
 3 #include <thread>
 4  
 5 std::atomic<bool> x = {false};
 6 std::atomic<bool> y = {false};
 7 std::atomic<int> z = {0};
 8  
 9 void write_x()
10 {
11     x.store(true, std::memory_order_release);
12 }
13  
14 void write_y()
15 {
16     y.store(true, std::memory_order_release);
17 }
18  
19 void read_x_then_y()
20 {
21     while (!x.load(std::memory_order_acquire))
22         ;
23     if (y.load(std::memory_order_acquire))
24         ++z;
25 }
26  
27 void read_y_then_x()
28 {
29     while (!y.load(std::memory_order_acquire))
30         ;
31     if (x.load(std::memory_order_acquire))
32         ++z;
33 }
34  
35 int main()
36 {
37     std::thread a(write_x);
38     std::thread b(write_y);
39     std::thread c(read_x_then_y);
40     std::thread d(read_y_then_x);
41     a.join(); b.join(); c.join(); d.join();
42     assert(z.load() != 0); 
43 }

运行代码,最后是可能出现z=0的情况的

C++六种内存序详解

 原因是read_x_then_y和read_y_then_x看到的x和y的修改顺序并不一致,也就是说read_x_then_y看到的是先修改了x再修改y, 而read_y_then_x看到的是先修改y再修改x, 这种现象出现的原因仍然可以归结到写操作的传播上,在某一时刻write_x的修改只传播到了read_x_then_y线程所在的processor,还没有传播到read_y_then_x线程所在的processor, 而write_y的修改只传播到了read_y_then_x线程所在的processor,还没有传播到read_x_then_y线程所在的processor. 

如果我们要保证最后z一定不等于0,需要将代码中std::memory_order_acquire和std::memory_order_release都换成std::memory_order_seq_cst, 这样才能保证read_y_then_x和read_x_then_y观察到的对x和y的修改顺序是一致的。

参考文档

https://en.cppreference.com/w/cpp/atomic/memory_order

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

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

https://www.zhihu.com/question/24301047/answer/83422523文章来源地址https://www.toymoban.com/news/detail-855038.html

到了这里,关于C++六种内存序详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从零开始理解Linux中断架构(1)-前言

    前言         前段时间在转行手撸WIFI路由器,搞wifi路由器需要理解网络驱动程序,以太网卡驱动程序,无线WIFI驱动程序,而网卡驱动的关键路径就在中断程序中,需要了解NIC设备驱动程序如何收发数据,为了彻底的知道数据包是如何二层传递上来的,又需要了解一点Lin

    2024年02月09日
    浏览(54)
  • 【C++】C/C++内存管理,从底层汇编带你理解new和delete!

    🎉博客主页:小智_x0___0x_ 🎉欢迎关注:👍点赞🙌收藏✍️留言 🎉系列专栏:C++初阶 🎉代码仓库:小智的代码仓库 我们先来通过下面代码来解答一些问题 这里我们补充一些内存中数据存放的位置的知识点 先来看第一题 globalVar 是定义在全局的变量所以他的作用域是全局

    2024年02月05日
    浏览(57)
  • (C++) 详解内存地址空间

    0. 概述 一个C/C++ 程序,编译之后,形成的程序,在执行期间,内存中不仅存在一块区域用于存放代码,还有一些其他的区域用于使用,本节会详解C/C++内部所使用的内存地址空间,关于各内存的作用、位置做一个整体概述。 1. C++ 内存布局 操作系统的内存布局可大致分为两块

    2024年02月22日
    浏览(33)
  • C++中的内存空间管理详解【C++】

    如图代码中变量在内存中的存储位置。 内存分布图: 1. 栈又叫做堆栈,存储非静态局部变量/函数参数/返回值等等,栈是向下增长的。 栈是向下增长的,而堆是向上增长的? 一般来说,在栈上开辟空间,先开辟的空间地址较高,而在堆开辟空间,先开辟的空间地址较低。

    2024年02月07日
    浏览(34)
  • 【C++】C++学习前言

    C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。

    2024年03月12日
    浏览(54)
  • c++内存的四大分区详解

    目录 前言: 1、程序的基本运行流程 2,为啥要分为四个区域? 3,分为哪四个区域? 4,4个区域详解 代码区: 为什么会设置这两个功能呢? 全局区: 栈区: 堆区:  new: 补充知识:new 总结: 这篇博客介绍 c++四大分区 的详解,其中也会涉及到有关 new 的知识

    2024年02月20日
    浏览(38)
  • 【c++中内存拷贝函数(C++ memcpy)详解】

    原型 :void*memcpy(void*dest, const void*src,unsigned int count);  功能 :由src所指内存区域复制count个字节到dest所指内存区域。   说明 :src和dest所指内存区域不能重叠,函数返回指向dest的指针。     举例 :  下面自行实现这个函数 程序清单 1 V0.1版程序  程序清单 2 测试V0.1用例   

    2023年04月20日
    浏览(32)
  • 【初阶C++】前言

    C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了 OOP(object oriented programming:面向对象)思想 ,支持面向对象的程序设计语言应运而生

    2024年02月04日
    浏览(61)
  • C++内存管理(2)new、delete详解

    目录 new operator(new操作) new类对象时加不加括号的差别 new工作任务 delete工作任务 new和delete 堆区空间操作(对比malloc和free) new和delete操作基本类型的空间 new和delete操作基本类型的数组 new和delete操作类的空间 new和delete操作对象数组 new内存分配细节探秘 为什么要尽可能少的

    2024年02月09日
    浏览(50)
  • 【自制C++深度学习框架】前言

    此GitHub项目是一个初学者的深度学习框架,使用C++编写,旨在为用户提供一种简单、易于理解的深度学习实现方式。以下是本项目的主要特点和功能: 计算图:使用计算图来描述深度学习模型的计算过程,利用计算图将神经网络的计算过程视为一个有向无环图。通过构建计算

    2024年02月07日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包