当前位置: 首页 > article >正文

C++ 内存顺序与内存模型

内存顺序(与多线程有关)

我们都知道,c++语法 多而杂,其中也有太多一致性……

C++内存顺序作用

我们首先来看看,内存顺序是干嘛用的            

【我们来进入以下场景:】

多线程程序中,所有线程共享同一片内存。但问题来了:

线程之间是怎么“看见”彼此的内存操作的?

操作顺序能不能保证?

如果顺序混乱,会不会导致数据出错?

如果不控制内存顺序,可能会发生以下尴尬场景:

A线程:我写了数据!

B线程:啥?你写了?我怎么没看到?

A线程:啊这,明明写了啊!

B线程:要么你糊了,要么我瞎了                                                                                                               

我们可以明显发现

在多线程编程中,编译器优化,CPU重排,缓存延迟等各个因素,可能使得线程对内存的观察顺序变的不一致,可能就会造成,线程A对数据做了操作,B却没及时看见----这就是内存可见性问题

由此引出了 内存顺序的核心内容

  1. 控制线程之间的内存可见性和执行顺序,避免数据争夺
  2. 平衡性能和正确性,在需要一致性多一点的地方适当牺牲一点性能和效率,反之则提高性能和效率

具体点的内存顺序如下:

多线程中的内存顺序是什么?

多线程中的内存顺序是指不同进程、共享内存块和其他内存空间如何排列在物理内存中的方式,以及不同内存类型(如char, int,

float)如何分配这些地址空间,从而影响数据的访问顺序和性能优化。

内存排列的关键点

1. **共享内存的分配**

   - 多线程程序需要使用共享段来共享内存块。每个进程可以使用不同的共享段或同一个共享段。

   - 每个共享段包含多个地址空间,这些地址空间由指针指向。

2. **内存段与地址空间的关系**

   - 指针类型(如int, char)决定了内存段的大小和类型。例如,int占1字节,char占1字节。

   - 当一个进程分配内存段时,其地址空间由对应的指针类型定义。

3. **跨进程共享的机制**

   - 多线程程序在访问共享内存块时需要确保各进程可以正确访问这些地址。这涉及到内存段的管理(如互斥锁、安全屏障)和内

存顺序的设计。

   - 精确地分配指针类型,使得不同的进程指向特定的地址空间,有助于减少缓存冲突。

4. **内存顺序在多线程中的应用**

   - 通过正确设计内存顺序,多线程程序可以优化数据的缓存访问路径,减少Cache碰撞。

   - 精确地将内存段和指针类型组合,确保每个进程能够高效地访问其分配的地址空间。

5. **跨进程共享的复杂性**

   - 多线程环境中的共享内存可能导致复杂的内存管理问题。例如,当进程A和进程B都访问同一个地址时,如何避免Cache冲突并确

保数据安全。

   - 通过使用正确的指针类型和设计适当的内存顺序,可以有效解决这些挑战。

### 实例解释

假设一个C++程序有两个多线程进 程AB,共享一个 shared memory块。进程A分配了一个128个地址的 shared segment,并使用

int 指针来指向这些地址。进程B也分配了另一个 128 个地址的 shared segment,并使用 char 指针来定义地址空间。

当进程A和进程B同时访问同一个 address 时,指针的类型不同(int char),这可能导致不同的Cache访问路径。为了避免冲突

,内存顺序的设计必须确保数据在两个进程之间能够正确访问,从而减少缓存冲突并提高性能。

C++的内存顺序分类

内存顺序模型提供了+多个级别,从松散到严格依次是

  • memory_order_relaxed:最轻量的内存顺序,不做同步保证,仅适用于操作无关的场景。
  • memory_order_acquire:确保加载操作之前的写操作不可重排到其后,适用于读取某个共享变量时的同步。
  • memory_order_release:确保存储操作之后的操作不可重排到其前,适用于写入共享变量时的同步。
  • memory_order_seq_cst:最严格的内存顺序,保证原子操作的顺序一致性,适用于需要确保严格同步的场景。

现在我们具体来看每一个:

       memory_order_relaxed (松散顺序)
  • 作用:不做任何同步操作,也不保证操作顺序。
  • 特点:允许编译器和 CPU 进行最大限度的优化,不保证某些操作之间的顺序,除非它们是相同的原子操作。
  • 适用场景:当你不关心操作顺序时,使用 memory_order_relaxed 可以获得更高的性能。它适用于那些独立的、无依赖关系的操作,常见于无锁编程和性能优化。

例子

std::atomic<int> x = 0;

x.store(1, std::memory_order_relaxed); // 无同步保证

// 多线程调用

counter.fetch_add(1, std::memory_order_relaxed);
          memory_order_acquire/release  有秩序的配合型

Acquire(获取): 确保后续的操作不会被重排到它之前。

Release(释放): 确保之前的操作不会被重排到它之后。

1. memory_order_acquire(获取顺序)

作用:memory_order_acquire 用于 加载(load) 操作时,它确保当前线程在读取原子变量之前的所有内存操作(写操作)都不会被重排到该操作之后。换句话说,加载操作之前的所有操作必须在加载操作之前完成。

特点:主要用于同步其他线程写入的操作(通常是release 顺序的操作)。

acquire 保证了数据的可见性:如果某个线程加载了一个带有 acquire 的原子变量值,那么之前所有的内存操作都对该线程可见。

可以用于确保线程在读取某个共享变量时,读取之前的所有操作已经完成。

例子:

std::atomic<int> flag = 0;

std::atomic<int> data = 0;



// 线程1

data.store(42, std::memory_order_release); // 写操作,释放顺序

flag.store(1, std::memory_order_release);  // 标志位设置为1,释放顺序



// 线程2

while (flag.load(std::memory_order_acquire) == 0) {  // 读取flag,获取顺序

                                 // 等待线程1完成标记

}

int x = data.load(std::memory_order_relaxed);  // 读取数据

在这个例子中,线程1执行 data.store(42, std::memory_order_release) 和 flag.store(1, std::memory_order_release),线程2则通过 flag.load(std::memory_order_acquire) 来判断线程1是否完成操作。acquire 确保了线程2在读取 flag 之前,线程1的所有操作(如 data.store)已经完成。没有 acquire,线程2可能会在 flag 被更新之前读取到旧值,导致数据同步不一致。

2. memory_order_release(释放顺序)

作用:memory_order_release 用于 存储(store) 操作时,它确保当前线程在存储原子变量之前的所有内存操作(读写)都不会被重排到该操作之后。换句话说,存储操作之前的所有操作必须在存储操作之前完成。

特点:

主要用于同步其他线程读取的操作(通常是由 acquire 顺序的操作)。

release 确保了线程间对共享数据的更新可见性:它保证了在当前存储操作之前的所有写操作对其他线程可见。

例子:

std::atomic<int> flag = 0;

std::atomic<int> data = 0;



// 线程1

data.store(42, std::memory_order_release); // 写操作,释放顺序

flag.store(1, std::memory_order_release);  // 标志位设置为1,释放顺序



// 线程2

while (flag.load(std::memory_order_acquire) == 0) {  // 读取flag,获取顺序

    // 等待线程1完成标记

}



int x = data.load(std::memory_order_relaxed);  // 读取数据

在这个例子中,线程1通过 store 将数据存储到 flag 和 data 中。由于使用了 memory_order_release,线程1确保它的写操作(包括对 data 的修改)对线程2是可见的。线程2使用 memory_order_acquire 来确保在读取 flag 之前,它能够看到线程1对 data 的更新。

acquire release 的配合

通常,memory_order_acquire 和 memory_order_release 一起使用来同步多个线程之间的操作。一个典型的模式是:

一个线程使用 memory_order_release 写数据,表明它的所有修改在之后对其他线程可见。

另一个线程使用 memory_order_acquire 读取数据,确保它能看到先前线程做出的修改。

这确保了在多线程环境下,操作的执行顺序保持一致,不会发生操作重排或数据可见性问题。

memory_order_seq_cst(顺序一致性)

这是最严格的内存顺序模型,也就是默认的内存顺序。它保证了所有线程对共享内存的操作(无论是加载还是存储)都严格按照程序的顺序来执行。

1. 顺序一致性的定义

顺序一致性要求,程序中所有线程的所有操作(加载、存储等)在所有线程中都有一个统一的顺序,且这个顺序与它们在代码中出现的顺序一致。也就是说,所有线程看到的操作顺序应该是全局一致的。具体来说,操作顺序必须遵循:

  • 每个线程中的操作顺序应该保持一致。
  • 所有线程看到的所有操作的顺序应该是相同的。

简而言之: 在使用 memory_order_seq_cst 时,不会发生任何操作重排,线程间的操作顺序是全局一致的。

2. 默认的内存顺序

在 C++11 中,原子操作的默认内存顺序是 memory_order_seq_cst。这意味着,除非显式指定其他内存顺序(如 acquire、release、relaxed 等),否则原子操作默认会使用顺序一致性。这对于开发者来说,提供了最大的安全性,因为它避免了内存重排和潜在的同步错误。

3. memory_order_seq_cst 与其他内存顺序的比较

  • memory_order_seq_cst vs memory_order_relaxed:memory_order_relaxed 允许操作重排,因此多个线程的操作顺序可能与程序中的顺序不一致,而 memory_order_seq_cst 保证严格按照程序顺序执行。
  • memory_order_seq_cst vs memory_order_acquire/memory_order_release:memory_order_acquire 和 memory_order_release 提供的同步机制相对较弱,仅仅保证某些操作之间的顺序(例如一个线程的写操作对另一个线程的读操作可见),但 memory_order_seq_cst 则是全局严格的一致性保证。

4. 顺序一致性的特性

  • 全局顺序一致性:所有线程都能看到操作的统一顺序,且该顺序与程序中的顺序一致。即使线程间有并发操作,它们的操作顺序也会按照一致的规则进行排序。
  • 避免操作重排:memory_order_seq_cst 会防止编译器、处理器等进行优化时的重排操作,确保操作严格按照程序中的顺序执行。
  • 程序一致性:对于很多应用场景,顺序一致性是最简洁、最安全的内存顺序,避免了手动控制更细粒度的同步。

memory_order_seq_cst 的应用场景

通常,使用 memory_order_seq_cst 是一种安全的选择,适用于以下情况:

  • 简化多线程程序的同步:由于它不允许任何形式的操作重排,所以它是多线程编程中最常用的内存顺序,尤其适用于那些不希望手动处理内存顺序的应用。
  • 无需考虑性能优化的场景:如果程序中不考虑性能的极致优化,memory_order_seq_cst 提供了最简单的同步方式。

使用 memory_order_seq_cst

以下是一个简单的示例,展示了使用 memory_order_seq_cst 来确保多线程操作的顺序一致性:


#include <iostream>

#include <atomic>

#include <thread>



std::atomic<int> flag(0);  // 用于线程同步

std::atomic<int> data(0);  // 存储共享数据



void thread1() {

    data.store(42, std::memory_order_seq_cst);  // 写操作

    flag.store(1, std::memory_order_seq_cst);   // 设置标志

}



void thread2() {

    while (flag.load(std::memory_order_seq_cst) == 0) {  // 获取标志

        // 等待 flag 被更新

    }

    std::cout << "Data: " << data.load(std::memory_order_seq_cst) << std::endl;  // 读取数据

}



int main() {

    std::thread t1(thread1);

    std::thread t2(thread2);



    t1.join();

    t2.join();



    return 0;

}

在此示例中,线程 1 使用 memory_order_seq_cst 写入数据和标志,线程 2 使用 memory_order_seq_cst 读取标志并打印数据。memory_order_seq_cst 保证了线程 2 在读取数据时,线程 1 对 flag 和 data 的更新对线程 2 是可见的,而且所有操作的顺序是一致的。

进一步思考的疑惑

为什么要这么多“花里胡哨”的顺序?

所以,其实是:

         为了提供更多的性能优化空间。不同场景下,有不同的安全性选择:

  1. 放松一致性要求,换取性能(如relaxed)。
  2. 保证数据同步,防止乱序(如acquire/release)。
  3. 强一致性要求,确保所有线程看到一致的顺序(如seq_cst)。

讲完了理论和使用,现在该进一步了,让我们来撸源码。就可以全局浏览内存顺序            是怎么实现的了!!!

在我们的原子操作中,我们可以发现,实际上有6种内存顺序模型,然后放置在一个枚举中

首先我们调用包含库函数   <atomic>,并正确使用上文提到的方法

然后我们右键关键字进入定义

然后就能清晰的看见这定义的枚举了

std::memory_order</code>有6个枚举值:

std::memory_order_relaxed、

std::memory_order_consume、

<code>std::memory_order_acquire</code>、

std::memory_order_release,

std::memory_order_acq_rel,

std::memory_order_seq_cst。

它们定义了不同的内存顺序语义。从最弱的到最强的。

官方文档这样定义

底层原理:

1. 内存顺序的基础:内存模型

C++11 引入了内存模型,以便在多线程环境中对内存访问进行更细粒度的控制。通过 std::memory_order,我们能够精确指定原子操作时如何与其他线程的操作进行同步。每个内存顺序枚举值代表了不同的同步策略,主要影响的是以下方面:

  • 内存屏障(Memory Barriers): 这是控制 CPU 和编译器内存访问顺序的机制。内存屏障指示处理器不要对特定的内存操作进行重排序。
  • 缓存一致性(Cache Coherence): 保证一个线程对内存的修改对其他线程可见的方式。

C++ 的原子操作和 memory_order 通过硬件和编译器提供的机制来实现不同的同步行为。

2. 内存顺序枚举的实现原理

std::memory_order 枚举值并不是直接操作内存,而是告诉编译器和硬件如何处理内存操作的顺序

3. 枚举值背后的实现机制

std::memory_order 是如何通过硬件和编译器来实现不同的同步行为的呢?这背后通常涉及以下几个关键技术:

3.1 内存屏障(Memory Barriers)

内存屏障(或称为内存栅栏)是防止编译器和处理器重排序指令的机制。不同的内存顺序会触发不同类型的内存屏障。

  • memory_order_relaxed:不会插入内存屏障,因此允许操作在不考虑同步的情况下重排。
  • memory_order_acquire:会在原子操作之前插入一个读取屏障,防止之前的操作被重排到此操作之后。
  • memory_order_release:会在原子操作之后插入一个写入屏障,防止后面的操作被重排到此操作之前。
  • memory_order_seq_cst:会插入最强的全序一致性屏障,保证严格的顺序一致性。

处理器提供了低级指令来实现内存屏障,例如:

  • mfence:用于内存写入的屏障。
  • lfence:用于读取操作的屏障。
  • sfence:用于数据写入的屏障。

这些指令控制了指令执行的顺序,确保了内存的可见性和同步。

3.2 硬件的缓存一致性协议

现代处理器通常会采用缓存一致性协议(如 MESI 协议)来确保多个处理器核访问共享内存时的数据一致性。std::memory_order 枚举中的一些操作(如 acquire 和 release)依赖于这些协议,保证数据在多个核心间的一致性。

例如,在 memory_order_release 下,当一个线程更新了共享数据,它会将该数据标记为 "已发布"。其他线程在 memory_order_acquire 操作时,会在读取共享数据之前先进行同步操作,从而确保其他线程能够看到最新的值。

3.3 编译器指令和优化

编译器会根据内存顺序的不同插入适当的同步指令或内存屏障。例如,如果你使用 memory_order_acquire,编译器会确保该操作之前的所有内存读取都不会被重排到它之后。而如果使用 memory_order_seq_cst,则会插入最强的同步屏障,保证所有线程看到完全相同的内存操作顺序。

4. 为什么通过枚举就能实现同步和异步

通过 std::memory_order 枚举,我们实际上是在告诉编译器和硬件如何处理原子操作的同步性和顺序性。每个枚举值对应不同级别的同步要求

  • memory_order_relaxed 不要求任何同步,允许操作重排,类似于“异步”。
  • memory_order_acquire 和 memory_order_release 则实现了更严格的同步,确保内存操作的可见性和顺序,类似于“同步”。
  • memory_order_seq_cst 是最严格的同步保证,确保所有线程对内存操作的顺序一致。

最终,通过这些枚举值,我们让编译器和硬件知道哪些操作可以重排,哪些操作必须保持顺序,哪些需要在处理器之间同步,从而实现我们所需要的同步或异步行为。

Last But NOT Least

内存顺序其实就是我们与硬件优化之间的竞争,需要性能,我们就不那么严格的同步,需要完美的同步,有秩序的线程,那就要牺牲一点性能开销!!!!

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             


http://www.kler.cn/a/537540.html

相关文章:

  • 详细教程 | 如何使用DolphinScheduler调度Flink实时任务
  • WPF模板
  • PHP PDO 教程
  • apisix网关ip-restriction插件使用说明
  • 确保数据一致性:RabbitMQ 消息传递中的丢失与重复问题详解
  • 10. 神经网络(二.多层神经网络模型)
  • k8s的操作指令和yaml文件
  • Vue(6)
  • 使用 JFreeChart 创建动态图表:从入门到实战
  • 深入解析 STM32 GPIO:结构、配置与应用实践
  • WebStorm设置Vue Component模板
  • 入门简单-适合新手的物联网开发框架有多少选择?
  • shell解决xml文本中筛选的问题
  • (14)gdb 笔记(7):以日志记录的方式来调试多进程多线程程序,linux 命令 tail -f 实时跟踪日志
  • 如何使用 Spring Boot 实现异常处理?
  • 前端开发架构师Prompt指令的最佳实践
  • 激活函数篇 03 —— ReLU、LeakyReLU、ELU
  • ffmpeg合成视频
  • 人工智能A*算法 代价函数中加入时间因素和能耗因素
  • Spring Boot 的问题:“由于无须配置,报错时很难定位”,该怎么解决?
  • vue3+vite+eslint|prettier+elementplus+国际化+axios封装+pinia
  • 23.PPT:校摄影社团-摄影比赛作品【5】
  • 设计模式-责任链模式:让请求像流水线一样自由流转
  • 19 角度操作模块(angle.rs)
  • 在 Open WebUI+Ollama 上运行 DeepSeek-R1-70B 实现调用
  • Unity项目接入xLua的一种流程