Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >原子变量——内存模型

原子变量——内存模型

作者头像
程序员的园
发布于 2024-11-08 08:12:50
发布于 2024-11-08 08:12:50
25500
代码可运行
举报
运行总次数:0
代码可运行

在多线程编程中,对共享数据的并发访问需要特别注意顺序性和可见性。现代处理器和编译器为了提升性能,往往会对代码指令进行重排,这种重排可能会影响不同线程对共享数据的观察顺序,导致未定义行为。C++11引入了内存模型,其定义了一组以标准化多线程环境下的内存访问规则,控制多线程程序中数据的访问顺序和可见性,保证多线程程序中对共享数据的访问顺序与程序逻辑一致。避免数据竞争和未定义行为,进而保障数据的一致性和程序的正确性。

1. 内存序

内存模型的核心概念为内存序,通过内存序可以控制原子变量的读取和写入操作的顺序,从而保证多线程环境下的同步和顺序性。

C++11中的内存模型通过std::memory_order枚举定义了如下内存序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum memory_order {
 memory_order_relaxed,
 memory_order_consume,
 memory_order_acquire,
 memory_order_release,
 memory_order_acq_rel,
 memory_order_seq_cst
};

以上六种内存序分别代表不同的同步和顺序关系,用于控制多线程程序中数据的访问顺序。

1.1 Relaxed

memory_order_relaxed允许读写操作独立执行,不进行同步,是最松散的内存序。它只满足原子性而不关心可见性和顺序性的场景。代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
std::atomic<int> x(0);
x.store(10, std::memory_order_relaxed); // 松散内存序
int y = x.load(std::memory_order_relaxed); // 松散内存序

1.2 Acquire

memory_order_acquire用于加载(load)共享数据,不可用于写数据——store函数。其保证当前线程中加载操作之后的任何读取和写入都不会被重排到加载操作之前;其他线程的所有release同一原子变量的写入操作为当前线程可见。适用于多线程的同步场景中确保前置依赖操作。代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
std::atomic<int> flag(0);
int data;
if (flag.load(std::memory_order_acquire) == 1) {
 // 获取内存序使得 data 可见
 int read_data = data;
}

1.3 Release

memory_order_release用于写入(store)共享数据,不可用于读数据——load函数。其保证当前线程此store 之前的读写操作不会被重排到 store 之后,意味着在执行 store 操作之前,所有之前的写入操作在本线程中都是可见的;其他线程的所有acquire同一原子变量的 load 操作时保证这些写入的可见性。其适用于向其他线程发布数据。代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
std::atomic<int> flag(0);
int data = 42;
data = 42;
flag.store(1, std::memory_order_release); // 发布 data

1.4 Acquire-Release

memory_order_acq_rel结合了Acquire和Release的特性,其应用于读——修改——写(Read-Modify-Write)操作,如fetch_add、fetch_sub、compare_excahnge等一个操作中既有读取又写入的情境下。它保证当前线程的读写操作不会重排到加载操作(load)之前存储操作(store)之后。其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量线程。适用于同时包含读取和写入的复杂同步场景。代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
std::atomic<int> value(0);
value.fetch_add(1, std::memory_order_acq_rel); // 同时获取和释放

1.5 Consume

memory_order_consume用于读取共享数据,不可用于写数据——store函数。其保证当前线程中load操作之后的任何读取和写入都不会被重排到加载操作之前;其他线程的所有释放同一原子变量的写入为当前线程可见。通常只影响编译器优化,不常用。

1.6 Sequentially Consistent

memory_order_seq_cst是C++11中引入的最强内存序,其可同时应用于加载(load)和存储(store)操作,当其应用于加载load时等价于memory_order_acquire,其应用于存储store时等价于memory_order_release。当前线程的读写操作不能重排于加载操作之前,写入操作之后;其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量的其他线程。适用于高安全性需求的场景,但性能开销较大。代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
std::atomic<int> counter(0);
counter.store(100, std::memory_order_seq_cst); // 严格一致的顺序

2. 内存模型的核心问题

内存模型的核心问题包括:可见性、顺序性和原子性。

  • 可见性:在多线程环境中,线程对共享变量的修改并不一定能被其他线程立即可见。使用std::memory_order_acquire和std::memory_order_release内存序可以确保共享变量在不同线程间的可见性。例如,生产者线程通过memory_order_release发布某些共享数据,而消费者线程则通过memory_order_acquire读取这些数据,从而保证数据在线程间的可见性。
  • 顺序性:顺序性是指操作执行的顺序保证在不同线程间一致。C++内存模型中,std::memory_order_seq_cst(顺序一致性)提供了严格的顺序保证,所有线程都观察到相同的操作顺序,避免数据的重排问题。在多线程中,顺序性是保证逻辑一致的重要因素。
  • 原子性:原子性确保操作的不可分割性,即在多线程环境下,操作要么全部完成,要么全部不执行。C++内存模型中的所有原子操作都具备不可分割性,避免了在多线程环境中发生数据竞争的风险。原子操作的不可分割性为多线程编程提供了基础的线程安全保障。

3. 代码示例

以下代码示例展示了不同内存序的应用及其在实际场景中的效果,通过一个简单的示例说明了内存序在可见性和顺序性方面的影响。

3.1 Relaxed

以下代码展示了std::memory_order_relaxed内存序的效果。由于采用松散的内存序,在不同线程间,变量x和y的更新可能存在不一致性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
#include <thread>
#include <iostream>
std::atomic x(0), y(0);
void write_x() { x.store(1, std::memory_order_relaxed); }

void write_y() { y.store(1, std::memory_order_relaxed); }

void read_x_then_y() 
{
 while (x.load(std::memory_order_relaxed) != 1); // 等待 x 被写入
 if (y.load(std::memory_order_relaxed) == 0) 
 {
 std::cout << "y is 0\n"; // 该情况可能发生
 }
}

int main() 
{
 std::thread t1(write_x);
 std::thread t2(write_y);
 std::thread t3(read_x_then_y);
 t1.join();
 t2.join();
 t3.join();
}

在上述代码中,memory_order_relaxed允许变量x和y在不同线程间的可见性不一致,可能导致读线程read_x_then_y看到x更新但看不到y的更新(y的值仍未0)。

在MSVC2022中,确实出现了打印“y is 0”的情况

3.2 Acquire && Release

以下代码展示了使用memory_order_acquire和memory_order_release的生产者-消费者模型,通过这两种内存序确保线程间的顺序性和数据可见性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
#include <thread>
#include <iostream>
std::atomic data(0);
std::atomic ready(false);
void producer() 
{
 data.store(42, std::memory_order_relaxed); // 松散存储数据
 ready.store(true, std::memory_order_release); // 发布 data
}

void consumer() 
{
 while (!ready.load(std::memory_order_acquire)); // 获取 ready 状态
 std::cout << "Data: " << data.load(std::memory_order_relaxed) << "\n";
}

int main() 
{
 std::thread t1(producer);
 std::thread t2(consumer);
 t1.join();
 t2.join();
 }

通过Release和Acquire内存序的组合,producer和consumer线程对于同一原子变量ready的读写操作使用了memory_order_acquire和memory_order_release,保证了consumer对ready的获取操后,可见data的写入。这种同步机制确保了线程间的可见性。

3.3 Sequentially Consistent

以下示例展示了顺序一致内存序的作用。在顺序一致性下,所有操作按全局顺序执行,确保了所有线程的观察顺序一致性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <atomic>
#include <thread>
#include <iostream>
std::atomic counter(0);

void increment()
{
 for (int i = 0; i < 100; ++i) 
 {
   counter.fetch_add(1, std::memory_order_seq_cst);
 }
}

int main()
{
 std::thread t1(increment);
 std::thread t2(increment);
 t1.join();
 t2.join();
 std::cout << "Counter: " << counter.load(std::memory_order_seq_cst) << "\n";
}

在此示例中,std::memory_order_seq_cst确保了所有线程对counter的操作按顺序执行,避免了重排问题,保证了线程间的数据一致性。所有线程都按严格的顺序对counter进行操作,从而得到期望的结果。

4. 总结

本文通过对C++内存模型及其内存序的详细解析,探讨了多线程环境下的可见性、顺序性和原子性问题。通过代码示例展示了不同内存序的实际应用效果,帮助开发者理解如何合理选择和使用内存序,以确保多线程程序的正确性和性能。正确理解并合理使用内存模型是保障多线程程序可靠性的关键。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
聊聊内存模型和内存序
最近群里聊到了Memory Order相关知识,恰好自己对这块的理解是模糊的、无序的,所以借助本文,重新整理下相关知识。
高性能架构探索
2022/06/17
2.7K1
深入理解C11/C++11内存模型
现代计算机体系结构上,CPU执行指令的速度远远大于CPU访问内存的速度,于是引入Cache机制来加速内存访问速度。除了Cache以外,分支预测和指令预取也在很大程度上提升了CPU的执行速度。随着SMP的出现,多线程编程模型被广泛应用,在多线程模型下对共享变量的访问变成了一个复杂的问题。于是我们有必要了解一下内存模型,这是多处理器架构下并发编程里必须掌握的一个基础概念。
Linux阅码场
2020/06/04
2.7K0
C++11内存模型
最近看了极客时间——《现代C++实战三十讲》中的内存模型与Atomic一节,感觉对C++的内存模型理解还不是很清楚,看了后面的参考文献以及看了一些好的博客,算是基本了解了,根据参考文献整合一下。更多细节可以看看参考文献。
ClearSeve
2022/02/11
8420
C++11内存模型
内存模型的内存序选择技巧
C++中的std::memory_order提供了多种内存序,通过合理的选择和使用,开发者能够根据多线程应用需求控制操作的顺序和可见性。在多线程应用中,选择合适的内存序既能保证数据的安全性,又能避免不必要的同步开销。本文在简要回顾各类内存序特性的基础上,重点介绍其在实际编程中的选择技巧。
程序员的园
2024/11/19
2320
内存模型的内存序选择技巧
《C++并发编程实战》读书笔记(3):内存模型和原子操作
C++标准中对象定义为某一存储范围。每个变量都是对象,每个对象都占用至少一块内存区域,若变量属于内建基本类型则仅占用一块,相邻的位域属于同一块。
C语言与CPP编程
2023/08/10
4660
《C++并发编程实战》读书笔记(3):内存模型和原子操作
内存序不再晦涩!用“锁语义”给你讲透
随着多核处理器的发展,多线程编程成为了一种常见的编程方式。但是,多线程编程必然面临数据同步问题,锁作为常见且易用的同步机制,在多线程编程中扮演着重要的角色。但是,锁的开销大,性能较差。原子变量作为一种低开销的同步机制,在多线程编程中也扮演着重要的角色。
程序员的园
2025/05/15
1410
内存序不再晦涩!用“锁语义”给你讲透
​C++ memory order 勘误
大约半年前写了一篇 C++11 memory order,有一定数量的人看过,尽管其中大部分内容无误,不过复查发现一些描述不准确或者不妥当的地方,而微信文章又不能修改 20 字以上,为避免传讹,想想还是把之前的文章删掉了。
JIFF
2020/05/20
7550
ARMv8 内存系统学习笔记
Normal memory 可以设置为 cacheable 或 non-cacheable,可以按 inner 和 outer 分别设置。
刘盼
2023/09/11
5940
ARMv8 内存系统学习笔记
原子变量——原子操作
在原子变量一中做了原子变量的科普介绍,仅仅将普通变量升级为原子变量,便解决了多线程环境下的数据竞争问题。在应对如上的简单案例时,仅仅使用原子变量重载的操作++即可,为了应对更加复杂的使用场景,C++标准库提供了丰富的原子变量操作,使之无需加锁便可在多线程环境中操作共享数据。本文将对这些原子变量操作做更详细的说明。
程序员的园
2024/11/07
3180
原子变量——原子操作
深入理解C11/C++11内存模型(白嫖新知识~)
现代计算机体系结构上,CPU执行指令的速度远远大于CPU访问内存的速度,于是引入Cache机制来加速内存访问速度。除了Cache以外,分支预测和指令预取也在很大程度上提升了CPU的执行速度。随着SMP的出现,多线程编程模型被广泛应用,在多线程模型下对共享变量的访问变成了一个复杂的问题。于是我们有必要了解一下内存模型,这是多处理器架构下并发编程里必须掌握的一个基础概念。
嵌入式Linux内核
2022/09/23
4530
深入理解C11/C++11内存模型(白嫖新知识~)
[译]C++中的内存同步模式(memory order)
原子变量同步是内存模型中最让人感到困惑的地方.原子(atomic)变量的主要作用就是同步多线程间的共享内存访问,一般来讲,某个线程会创建一些数据,然后给原子变量设置标志数值(译注:此处的原子变量类似于一个flag);其他线程则读取这个原子变量,当发现其数值变为了标志数值之后,之前线程中的共享数据就应该已经创建完成并且可以在当前线程中进行读取了.不同的内存同步模式标识了线程间数据共享机制的"强弱"程度,富有经验的程序员可以使用"较弱"的同步模式来提高程序的执行效率.
用户2615200
2022/01/12
1.3K0
蓝桥ROS机器人之现代C++学习笔记7.5 内存模型
学习代码如下: #include <atomic> #include <thread> #include <vector> #include <iostream> using namespace std; using namespace std::chrono; const int N = 10000; void relaxed_order() { cout << "relaxed_order: " << endl; atomic<int> counter = {0}; ve
zhangrelay
2022/04/29
2340
蓝桥ROS机器人之现代C++学习笔记7.5 内存模型
C++多线程并发(五)—原子操作与无锁编程
前面介绍了多线程间是通过互斥锁与条件变量来保证共享数据的同步的,互斥锁主要是针对过程加锁来实现对共享资源的排他性访问。很多时候,对共享资源的访问主要是对某一数据结构的读写操作,如果数据结构本身就带有排他性访问的特性,也就相当于该数据结构自带一个细粒度的锁,对该数据结构的并发访问就能更加简单高效,这就是C++11提供的原子数据类型< atomic >。下面解释两个概念:
全栈程序员站长
2022/06/27
2.5K0
C++多线程并发(五)—原子操作与无锁编程
C++ 新特性学习(八) — 原子操作和多线程库[多工内存模型]
分别对于两个进程而言,可观察行为确实没有变化。而这种优化在某些时候确实会有比较明显的效果。但是很显然,语义变化了。在原来的结果里不可能发生 x和y都为0的情况,而优化过后,有可能出现。 再来个例子:
owent
2023/03/05
3510
C++11原子操作:从入门到精通
原子操作(Atomic Operations)是指不可被中断的一个或一系列操作。在多线程编程中,原子操作就像是"不可分割的最小单位",要么完全执行,要么完全不执行,不会出现执行到一半被其他线程打断的情况。
码事漫谈
2025/06/25
1900
C++11原子操作:从入门到精通
What is the Memory Model in C++11
C++11其实主要就四方面内容,第一个是可变参数模板,第二个是右值引用,第三个是智能指针,第四个是内存模型(Memory Model)。
C语言与CPP编程
2020/12/28
4560
What is the Memory Model in C++11
C++11多线程内存模型:从入门到精通
在当今的软件开发领域,多线程编程已经成为了提升程序性能和响应能力的重要手段。然而,多线程环境下的内存访问和同步问题却给开发者带来了诸多挑战。C++11标准的出现,为多线程编程带来了重大变革,其中内存模型的改进尤为关键。本文将带领小白们从入门到精通,深入了解C++11多线程内存模型。
码事漫谈
2025/06/18
1960
C++11多线程内存模型:从入门到精通
UNIX(多线程):27---多线程并发之原子操作与无锁编程
原子操作:顾名思义就是不可分割的操作,该操作只存在未开始和已完成两种状态,不存在中间状态;
用户3479834
2021/02/03
5950
UNIX(多线程):27---多线程并发之原子操作与无锁编程
C++ 内存模型
在《C++ 并发编程》一文中,我们已经介绍了C++11到C++17在并发编程方面的新增API。
C语言与CPP编程
2021/10/09
2.4K0
C++ 新特性学习(八) — 原子操作和多线程库[多工内存模型]
分别对于两个进程而言,可观察行为确实没有变化。而这种优化在某些时候确实会有比较明显的效果。但是很显然,语义变化了。在原来的结果里不可能发生 x和y都为0的情况,而优化过后,有可能出现。 再来个例子:
owent
2018/08/01
4610
相关推荐
聊聊内存模型和内存序
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档