C++11中的原子操作及其底层缓存一致性

news2025/1/16 18:51:55

C++中的原子变量(atomic variables)是一种并发编程中用于保证数据一致性和线程安全的机制。在多线程环境下,当多个线程同时访问或修改同一个变量时,可能会产生竞争条件(race condition),导致未定义的行为。C++中的原子变量通过提供一种无锁(lock-free)的机制,确保变量的访问和操作是原子的,即不可被中断,从而避免了竞争条件的发生。

一、C++原子变量的特点

  1. 原子性操作:原子变量支持一系列原子操作(atomic operations),例如读、写、加减、比较交换等,所有这些操作都能够保证在多线程环境中是不可分割的(atomic),即操作要么完全执行,要么不执行。
  2. 无锁机制:原子变量通常是无锁实现的,这意味着它们不会像互斥锁(mutex)那样导致线程的阻塞,从而提高了程序的性能。
  3. 线程安全:原子变量确保在线程并发访问同一变量时,不会发生数据竞争,也就是说,不需要使用额外的同步机制来保护它们。
  4. 内存序:C++的原子操作支持多种内存顺序模型,例如顺序一致性(sequential consistency)、获取-释放(acquire-release)等,以控制不同线程之间的操作如何在内存中排序。这使得开发者可以在性能和正确性之间做出权衡。

二、C++中的std::atomic类模板

C++11标准引入了<atomic>头文件,提供了std::atomic类模板和一组函数来操作原子变量。std::atomic是一个类模板,可以用于各种类型的变量,例如整型、指针、布尔型等。

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);  // 定义一个原子整型变量并初始化为0

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;  // 原子递增
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;  // 输出结果应该是2000
    return 0;
}

在上面的例子中,counter是一个std::atomic<int>类型的变量,它被两个线程并发递增。由于counter是原子变量,递增操作是线程安全的,最终的结果总是正确的2000,而不会出现数据竞争。

三、常见原子操作

  1. 加载和存储操作(load, store)
std::atomic<int> a(10);
int x = a.load();  // 原子读取
a.store(20);       // 原子写入
  1. 原子自增和自减操作(fetch_add, fetch_sub)
std::atomic<int> a(0);
a.fetch_add(1);  // 原子自增1
a.fetch_sub(1);  // 原子自减1
  1. 比较并交换(compare_exchange_weak, compare_exchange_strong): 比较并交换操作允许在线程间协调修改变量值:
std::atomic<int> a(0);
int expected = 0;
int desired = 1;
a.compare_exchange_strong(expected, desired);  // 如果a的值等于expected,则将其设为desired
  1. 交换(exchange)
std::atomic<int> a(0);
int old_value = a.exchange(2);  // 将a的值设为2,并返回旧值

四、原子性

原子性指的是一段指令即使映射到底层,在其他核心看来,要么全部没有没有开始,要么全部结束了,不会让让其他核心看到执行的一个中间状态。

4.1、单核处理器原子性实现

单核处理器上实现原子性只需要保证操作指令不被中断即可,可以使用自旋锁锁住操作,也可以直接关中断,保证其他线程不会进入cpu。

4.2、多核处理器原子性实现

多核处理器还需要避免其他核心操作相关内存,曾经是通过“锁总线”的方式实现的,锁住其他核心只允许当前核心访问总线。这种方法的弊端是其他核心连同非相关的内存空间也无法使用。“总线锁”这种方式在x86架构cpu中可以使用lock指令去实现。

五、存储体系

image-20241011200742147

可以看到一级缓存和二级缓存是每个核心独有的,三级缓存和内存是公有的,越往下,容量越大,速度越慢。

Cpu访问缓存的时候会有一个最小的读取单位,叫做cache line,一般是64个字节。cache line包含三部分

Flag标识的是当前的缓存是否可用。

Tag标识的是当前的缓存是否命中。

Data是缓存数据。

缓存的读写

核心计算完毕意味着要将计算好的数据写会内存,曾经的策略是写到缓存中,也会写到内存中,这样的话写性能会很低,现代计算机很少使用了。现在的策略是尽量把数据存储到缓存之中,如果能写到缓存之中就避免写道内存中。被修改的数据在缓存中会被标记为“脏”,表示它与主内存中的数据不一致。脏数据在缓存达到一定阈值、缓存被替换或系统需要进行内存同步时写回到主内存。这种方式可以提高系统性能,尤其在需要频繁读写的场景下。

write through 策略

每当cache收到写数据指令时,若写命中,则cpu会同时将数据写到cache和主存

write back 策略

只有在一个cache行被选中替换回主存时,如果cache行是脏数据,才将他写回主存。如果cache行没有被修改过不是脏数据那么不需要把数据写回主存,这样就有效降低了cache到主存的写次数。

image-20241011220554908

也可以看下面的广为流传的图

image-20241011220705774

六、缓存一致性问题-MESI协议

缓存一致性解决的是不同的核心的缓存中内容不一致的问题,这是由于write back策略和cpu多核属性造成的。该问题通过实现MESI一致性协议解决。相关的缓存一致性协议还有MSI,MOSI,Synapse,Firefly及DragonProtocol等。

6.1、写传播

某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的Cache,这个称为写传播(Write Propagation)。

写传播最常见的实行方式就是总线嗅探,当 core 1 中修改了 L1 cache 中 i 变量的值,通过总线把这个事件广播通知给其他所有的核心。每个 CPU core 都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 core 2 的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache。

其优点是简单,但是缺点是不论别的核心是否缓存相同的数据都需要发出一个广播事件,会加重总线的负担。

6.2、事物串行化

但是由此又引发了一个问题,如果多个核心同时修改了数据,其他核心该怎么办?这时候需要用事务串行化解决。事务串行化,让修改事件按时间的先后顺序发生。举个例子

  • 内存中有变量 i=1
  • core 1 将 i=100,同时 core 2 将 i=200
  • 上面两步都会将修改传播到 core 3 和 core 4
  • 如果 core 3 先看到的是 i=200 后看到的是 i=100 ,那么 core 3 中缓存的就是 i=100
  • 但是 core 4 收到的事件是反过来的,先看到的是 i=100 后看到的是 i=200 ,那么 core 4 中缓存的就是 i=200

可见,仅仅只有写传播的话,各个cache中的数据还是无法保持一致性,要实现一致性就必须引入事物串行化,保证 core 3 和 core 4 看到相同顺序的数据变化。这有两点要求

  • CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU core
  • 要引入“锁”的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了"锁",才能进行对应的数据更新

6.3、MESI状态表示

MESI协议基于总线嗅探机制实现了事务串形化,也用状态机机制降低了总线带宽压力,做到了 CPU 缓存一致性。

上面也提到了,CPU读取一个缓存其实不是直接读这个变量,而是这个变量所在的整个缓存行,其中MESI分别表示四种缓存行状态:

状态描述
M修改(Modefiy)该缓存行有效,数据被修改了,和内存中的数据不一样,数据只存在于本缓存行中
E独享(Exclusive)该缓存行有效,数据和内存中的数据一致,数据只存在本缓存行中
S共享(Shared)该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中
I无效(Invalid)该缓存行数据无效

M和E的数据都是本内核(core)独有的,区别在于M状态的数据是dirty(和内存中的不一致),E状态的数据是clean(和内存中的一致);S状态是所有Core的数据都是共享的,只有clean的数据才能被多个core共享;I表示这个Cache line无效。

“独占”和“共享”的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

6.4、MESI状态转换

b3e4d76661ca4e4ea3dddced613bf1a9

6.4.1、在状态I
  • local read:

    • 当其他核心没有这个缓存行时,CPU从内存取缓存行更新到cache,并把状态设置为E
    • 当其他核心存在这个缓存行时
      • 若其他核心是M状态,则同步其到主存,然后两个核心的状态都设置为S
      • 若其他核心是S或者E状态,则两个核心状态都变为S
  • local write:

    • 当其他核心没有这个缓存行时,CPU从内存中读取数据,缓存到cache,在cache中更新数据,状态设置为M
    • 当其他核心存在这个缓存行时
      • 若其他核心是M状态,则同步其到主存,然后将其他核心状态设置为I
      • 若其他核心是S或者E状态,则直接设置其他核心状态为I
  • remote read:

    • 既然是invalid,那么其他核心操作与它无关
  • remote write:

    • 既然是invalid,那么其他核心操作与它无关
6.4.2、在状态E
  • local read:

    • 从 cache 中取数据,状态不变
  • local write:

    • 从 cache 中读数据,状态变为M
  • remote read:

    • 数据和其他核心公用,状态变为S
  • remote write:

    • 数据被修改,本地核心的 cache line 中的数据不能再使用,状态变为I
6.4.3、在状态S
  • local read:

    • 从 cache 中取数据,状态不变
  • local write:

    • 从 cache 中读数据,状态变为M,其他核心贡献的 cache line 状态变为 I。
  • remote read:

    • 状态不变
  • remote write:

    • 数据被修改,本地核心的 cache line 中的数据不能再使用,状态变为I
6.4.3、在状态M
  • local read:

    • 从 cache 中取数据,状态不变
  • local write:

    • 修改Cache中数据,状态不变
  • remote read:

    • cache line 的数据被写到主存中,使其他核能使用到最新的数据,状态变为S
  • remote write:

    • cache line 的数据被写到主存中,使其他核能使用到最新的数据,由于其他核会修改这行数据,状态变为I

六、内存顺序模型

C++原子操作可以指定内存顺序模型,用于控制不同线程对共享数据的访问顺序,指导编译器进行cpu指令重排的优化。

编译器和cpu会在判断代码顺序不影响程序的情况下,有可能会重排相邻的代码执行顺序。比如一个线程锁住了一块内存空间,另一个线程无法操作那块内存空间,这项操作之后的操作并不刚需这块内存空间,这时候可能会重排先往后执行。这是为了提升整个系统的性能,但有时我们要求一定要按某个顺序执行,那么就有时候就不允许重排操作。

常见的内存顺序包括:

  1. 顺序一致性(std::memory_order_seq_cst):这是最严格的内存顺序,保证所有原子操作在所有线程上都以相同的顺序出现。它是默认的内存顺序,易于理解但性能可能较低。
  2. 松散顺序(std::memory_order_relaxed):允许不对线程之间的操作顺序做任何保证,适用于对性能要求较高但不要求严格顺序的情况。
  3. 获取-释放(std::memory_order_acquire, std::memory_order_release):用于同步线程间的数据依赖关系。acquire确保读取前的操作不会被重排序到读取后,release确保写入后的操作不会被重排序到写入前。

七、何时使用原子变量?

  • 原子变量适合用于在多线程环境中,对单个变量进行简单的操作(如递增、交换、读写)时,可以避免使用锁,从而提高性能。
  • 如果操作涉及多个变量的协调,或者是更复杂的共享资源操作,则需要使用更强的同步机制(如互斥锁或条件变量)。

八、总结

C++中的原子变量通过无锁的原子操作来保证多线程环境下的线程安全,并提供多种内存顺序模型供开发者根据需求进行性能调优。使用原子变量可以有效避免数据竞争,并简化多线程编程中的同步问题。

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

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

相关文章

Javascript 使用 Jarvis 算法或包装的凸包(Convex Hull using Jarvis’ Algorithm or Wrapping)

给定平面中的一组点&#xff0c;该集合的凸包是包含该集合所有点的最小凸多边形。 我们强烈建议您先阅读以下文章。 如何检查两个给定的线段是否相交&#xff1f; c https://blog.csdn.net/hefeng_aspnet/article/details/141713655 java https://blog.csdn.net/hefeng_aspne…

C#中判断的应用说明二(switch语句)

一.判断的定义说明 判断结构要求程序员指定一个或多个要评估或测试的条件&#xff0c;以及条件为真时要执行的语句&#xff08;必需的&#xff09;和条件为假时要执行的语句&#xff08;可选的&#xff09;。下面是大多数编程语言中典型的判断结构的一般形式&#xff1a; 二.判…

六、设置弹窗显示队列

实现原理&#xff1a; 把弹窗放置在一个队列中&#xff0c;通过设置UI方法&#xff0c;直接调用这个队列中的文本 Queue是一个先进先出的队列。 一、队列的使用 写一个增加提示的方法 在这里使用锁来执行这个语句&#xff0c;向这个队列里面增加一个提示文本 在这里进行文本…

Llama3-Factory模型部署新手指南

一、介绍 为了保持其公司在人工智能开源大模型领域的地位&#xff0c;社交巨头Meta推出了旗下最新开源模型。当地时间4月18日&#xff0c;Meta在官网上宣布公布了旗下最新大模型Llama 3。目前&#xff0c;Llama 3已经开放了80亿&#xff08;8B&#xff09;和700亿&#xff08;…

报表控件stimulsoft操作:使用 Stimulsoft 产品连接到 Json数据源

我们继续我们的系列文章&#xff0c;介绍如何在 Stimulsoft 产品中连接各种类型的数据源。在上一篇文章中&#xff0c;我们讨论了如何连接 OData。今天&#xff0c;我们将深入研究另一种类型的数据源 - JSON。 Stimulsoft Ultimate &#xff08;原Stimulsoft Reports.Ultimate…

Bat To Exe Converter软件:将批处理文件bat转换成exe应用程序

Bat To Exe Converter软件&#xff1a; 将批处理文件bat转换成exe应用程序&#xff08;文章最后附软件下载地址&#xff09;Bat To Exe Converter软件可以把批处理文件bat转换为exe可执行文件&#xff0c;甚至运用密码保护文件&#xff0c;生成的.EXE是独立的文件&#xff0c;…

DevExpress WinForms中文教程:Data Grid - 如何点击获取信息?

在使用DevExpress WinForms的Data Grid之类控件时&#xff0c;可能需要实现自定义用户交互&#xff0c;例如显示数据行的上下文菜单&#xff0c;或者在双击一行时调用编辑表单。在这些情况下&#xff0c;您需要在指定的坐标处标识网格元素。 在本教程中&#xff0c;您将学习如…

Python案例 | 使用K-means 聚类算法提取图像中的颜色

假如我们需要提取下图中的颜色&#xff0c;可以通过使用 K-means 聚类算法对图像进行颜色聚类分析&#xff0c;并生成一个基于聚类中心(即最具代表性的颜色)的RGB值和调色板。 # 通过使用 K-means 聚类算法对图像进行颜色聚类分析&#xff0c;并生成一个基于聚类中心(即最具代…

时间序列预测(二)——前馈神经网络(Feedforward Neural Network, FNN)

上一篇文章有提到线性回归方程。 时间序列预测&#xff08;二&#xff09;——前馈神经网络&#xff08;Feedforward Neural Network, FNN&#xff09;-CSDN博客 与线性回归相比&#xff1a; 线性回归只有一个线性层&#xff0c;输入直接映射到输出&#xff0c;不包含隐藏层…

「漏洞复现」灵当CRM data/pdf.php 任意文件读取漏洞

0x01 免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删…

网页内容获取:Scala自动化脚本的实现

对于开发者和数据科学家来说&#xff0c;自动化获取网页内容是一个常见的需求。Scala&#xff0c;作为一种多范式编程语言&#xff0c;以其强大的函数式编程特性和并发处理能力&#xff0c;成为了编写高效自动化脚本的理想选择。本文将介绍如何使用Scala结合Selenium WebDriver…

Element Ui el-table列表中的tooltip内容过长超出屏幕换行显示

elementui-table组件列表中的tooltip内容过长超出屏幕换行显示内容,虽然el-table列属性中带的有show-overflow-tooltip&#xff0c;可以设置内容超出列宽度显示为…&#xff0c;且有tooltip提示全部内容&#xff0c;但是内容过多时&#xff0c;提示会超出屏幕&#xff1a; 只有…

芒果YOLOv10改进136:注意力机制MLLA|即插即用:融合Mamba设计精髓的线性注意力视觉Transformer

💡本篇内容:芒果YOLOv10改进136:即插即用,最新注意力机制MLLA:融合 Mamba 设计精髓的线性注意力视觉Transformer MLLA|Mamba-Like Linear Attention 模块 | 即插即用 该模块将选择性的状态空间模型和线性注意力在一个统一公式中进行了重新阐述,将Mamba重新定义为具有六个主…

从头开始的可视化数据 matplotlib:初学者努力绘制数据图

从头开始学习使用 matplotlib 可视化数据&#xff0c;对于初学者来说&#xff0c;可能会有些挑战&#xff0c;但 matplotlib 的核心理念非常清晰&#xff1a;绘制图表需要了解如何设置图形、坐标轴以及如何用数据填充它们。我们可以通过一些简单的例子来逐步介绍基本步骤。 1. …

小程序底部导航按钮实现

商城小程序需要四个底部导航按钮&#xff0c;遂记录一下实现过程 最终实现效果如下所示 新建一个小程序项目&#xff0c;我是创建了JS模板&#xff0c;项目创建完成后需要新建五个文件夹&#xff0c;其中四个&#xff08;page子文件夹&#xff09;用于存放pages文件&#xff0…

探索光耦:光耦——不间断电源(UPS)系统中的安全高效卫士

在现代社会&#xff0c;不间断电源&#xff08;UPS&#xff09;系统已成为保障关键设备和数据安全的关键设施&#xff0c;广泛应用于企业数据中心、家庭电子设备等场景。UPS能在电力中断或波动时提供稳定电力&#xff0c;确保设备持续运行。而在这套系统中&#xff0c;光耦&…

C# 使用S7netplus读取西门子PLC的DB块-S7协议

在工业的设备监控领域&#xff0c;S7协议使用比较普遍下面简单教大家使用 1、添加包 2、添加帮助类(读写数据就研究该类即可&#xff0c;具体的理论就不多说了) //plc中类型与c#类型 bool > Bit //Byte > byte //word > ushort //DWord > uint //Int > short …

【AI知识点】三种不同架构的大语言模型(LLMs)的区别

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI大项目】【AI应用】 在自然语言处理&#xff08;NLP&#xff09;中&#xff0c;预训练语言模型&#xff08;LLMs, Large Language Models&#xff09;通常基于不同的架构&#xff0c;如仅编码器的模型&#xff08;Enc…

Java开发中知识点整理

正则表达式 测试网址 Git 分支和主分支有冲突 先checkout origin/分支把origin/master pull进本地分支 修改冲突MergeCommit and Push