0

0

C++原子操作与内存顺序memory_order使用

P粉602998670

P粉602998670

发布时间:2025-09-18 11:35:01

|

326人浏览过

|

来源于php中文网

原创

原子操作保证单个变量的读写不可中断,内存顺序控制多线程下操作的可见性与顺序,二者结合可在无锁情况下实现高效、正确的并发编程。

c++原子操作与内存顺序memory_order使用

C++的原子操作和内存顺序,在我看来,是多线程编程里一把双刃剑,它能让你在无锁并发的世界里翩翩起舞,也能在你一个不慎时,把你摔得七荤八素。简单来说,原子操作保证了单个变量的读写或修改是不可中断的,就像一个事务,要么完成,要么不发生。而内存顺序(

memory_order
)则是用来告诉编译器和CPU,这些原子操作之间以及它们与其他非原子操作之间,应该以何种顺序被观察到,这直接决定了不同线程看到的数据一致性程度。理解并正确运用它们,是编写高性能、正确多线程代码的关键。

解决方案

要深入理解并运用C++的原子操作与内存顺序,我们首先得认识到它们是为了解决传统锁机制(如

std::mutex
)在某些场景下的性能瓶颈和粒度问题而生。C++11引入的
std::atomic
模板类及其相关的内存顺序枚举,提供了一套细粒度的并发控制手段。

核心在于

std::atomic<T>
类型,它确保了对
T
类型变量的任何操作(如读、写、修改)都是原子的。这意味着,即使在多线程环境下,一个线程对原子变量的操作也不会被其他线程“看到一半”。但仅仅原子性还不够,因为现代CPU和编译器为了性能,会进行指令重排和内存访问优化,这可能导致不同线程观察到操作的顺序与代码编写顺序不一致。这就是
memory_order
登场的原因。

memory_order
枚举定义了六种内存顺序:

立即学习C++免费学习笔记(深入)”;

  • memory_order_relaxed
    :最宽松的顺序,只保证操作本身的原子性,不提供任何跨线程的同步或排序保证。
  • memory_order_consume
    :消费顺序。读操作依赖于另一个线程的写操作,且只对数据依赖的后续操作提供排序。这个很复杂,实际中常被编译器优化为
    acquire
  • memory_order_acquire
    :获取顺序。一个线程的
    acquire
    操作能看到另一个线程在
    release
    操作之前的所有写操作。
  • memory_order_release
    :释放顺序。一个线程的
    release
    操作确保所有在此操作之前的写操作,对后续进行
    acquire
    操作的线程可见。
  • memory_order_acq_rel
    :获取-释放顺序。用于读-改-写操作,同时具备
    acquire
    release
    的语义。
  • memory_order_seq_cst
    :顺序一致性。最严格的顺序,保证所有
    seq_cst
    操作在一个全局总序中被观察到。这是
    std::atomic
    操作的默认内存顺序。

实际应用中,我们通常会结合

load()
store()
exchange()
compare_exchange_weak()
compare_exchange_strong()
fetch_add()
等原子操作成员函数来指定内存顺序。

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

std::atomic<int> counter(0); // 默认memory_order_seq_cst

void increment_counter() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 简单计数,不需要严格顺序
    }
}

std::atomic<bool> ready(false);
std::atomic<int> data(0);

void producer() {
    data.store(42, std::memory_order_release); // 写入数据,并释放内存顺序
    ready.store(true, std::memory_order_release); // 设置就绪标志,并释放内存顺序
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) { // 等待就绪,并获取内存顺序
        std::this_thread::yield(); // 避免忙等
    }
    // 此时,data.load()将保证看到42,因为ready的acquire与producer的release同步
    std::cout << "Data is: " << data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Final counter: " << counter.load() << std::endl; // 默认seq_cst加载

    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();

    return 0;
}

为什么我们需要原子操作,普通锁不够吗?

这确实是个好问题,初学者往往会疑惑,既然有

std::mutex
这样的锁可以保护共享数据,为什么还要引入原子操作和这么复杂的内存顺序呢?在我看来,这就像是修房子,锁是把整个房间锁起来,而原子操作则是给房间里某件特定物品加了个保险箱。

普通锁(互斥量)的粒度通常比较粗。它保护的是一段代码块,也就是所谓的“临界区”。当一个线程进入临界区时,它会获得锁,其他试图进入的线程就必须等待。这种机制简单有效,能确保临界区内的所有操作都是串行执行的,从而避免数据竞争。然而,它的缺点也很明显:

  1. 性能开销: 锁的获取和释放本身是有开销的,涉及到操作系统调用或复杂的同步指令。在高并发、临界区很短的场景下,锁的开销可能远大于实际业务逻辑的开销,导致性能下降。
  2. 粒度问题: 锁一次性保护了整个临界区,即使临界区内只有一小部分操作涉及共享数据,其他不相关的操作也可能被无谓地串行化,降低了并行度。
  3. 死锁风险: 使用锁时,需要非常小心地管理锁的顺序,否则很容易引入死锁问题,导致程序挂起。

原子操作则提供了一种“无锁(lock-free)”或“非阻塞(non-blocking)”的并发控制手段。它主要针对单个变量的读、写、修改操作。CPU提供了特殊的指令(例如,x86架构上的

LOCK
前缀指令,或者特定的CAS – Compare-And-Swap指令),这些指令能够保证在执行过程中不会被其他CPU核心中断。

对比来看:

  • 性能: 对于单个变量的简单操作,原子操作通常比加锁的开销小得多。它们避免了操作系统上下文切换、线程阻塞和唤醒的开销。在某些高并发场景下,无锁算法能展现出更好的扩展性。
  • 粒度: 原子操作的粒度是最小的,它只保护了单个变量的访问。这使得我们可以更精细地控制并发,提高程序的并行度。
  • 避免死锁: 因为没有“锁”的概念,自然也就没有死锁的风险。

当然,原子操作并非万能药。它们只适用于单个变量的操作。如果需要保护多个变量或复杂的复合操作,那么锁仍然是更直接、更安全的方案。原子操作的复杂性在于,一旦脱离了默认的

seq_cst
,你就必须深入理解内存模型和不同
memory_order
的语义,否则很容易引入难以调试的并发错误。所以,选择原子操作还是锁,本质上是一个权衡:在需要极致性能和精细控制的场景下,原子操作是强大的工具;而在追求开发效率和代码简洁性时,锁往往是更稳妥的选择。

PathFinder
PathFinder

AI驱动的销售漏斗分析工具

下载

深入理解C++内存模型:不同memory_order的实际影响与选择

C++内存模型这东西,坦白说,初次接触时会让人觉得有点玄乎,它描述的是多线程程序中,内存操作的可见性和顺序性规则。不同的

memory_order
就是我们用来与这个模型“对话”的语言,告诉它我们对这些规则的具体要求。理解它们实际影响,是避免并发bug的关键。

  1. std::memory_order_relaxed
    :性能至上,风险并存

    • 实际影响: 这是最弱的内存顺序。它只保证原子操作本身是不可分割的,但不保证任何操作之间的顺序性。这意味着,一个线程对
      relaxed
      原子变量的写入,可能在另一个线程观察到该写入之前,先观察到这个线程后续的非原子写入。编译器和CPU可以随意重排
      relaxed
      操作,只要不改变单个线程内的行为。
    • 选择场景: 适用于那些只关心最终结果,不关心中间状态或操作顺序的简单计数器。比如,一个全局的统计数字,每个线程只管加一,最终总和正确即可。
    • 风险: 如果你的代码中存在任何对数据可见性或操作顺序的隐式依赖,使用
      relaxed
      几乎必然导致难以复现的bug。
    std::atomic<int> x(0);
    std::atomic<int> y(0);
    
    void thread1() {
        x.store(1, std::memory_order_relaxed);
        y.store(1, std::memory_order_relaxed); // y可能在x之前被其他线程看到
    }
    
    void thread2() {
        while (y.load(std::memory_order_relaxed) == 0); // 等待y被写入
        // 此时x.load()可能仍为0,因为relaxed不提供排序保证
        if (x.load(std::memory_order_relaxed) == 0) {
            std::cout << "Surprise! x is still 0 even after y is 1." << std::endl;
        }
    }
  2. std::memory_order_release
    std::memory_order_acquire
    :构建同步屏障

    • 实际影响: 这是构建无锁数据结构和实现线程间通信的基石。
      release
      操作确保了所有在它之前的内存写入(包括非原子写入)都会在
      release
      操作完成前完成并对其他线程可见。
      acquire
      操作则保证了所有在它之后的内存读取(包括非原子读取)都会在
      acquire
      操作完成之后执行,并且能够看到对应
      release
      操作之前的所有写入。它们共同建立了一个“ happens-before ”关系。
    • 选择场景: 经典的生产者-消费者模型。生产者写入数据后,执行
      release
      操作,表示数据已准备好;消费者执行
      acquire
      操作来检查数据是否准备好。一旦
      acquire
      成功,消费者就能保证看到生产者在
      release
      之前写入的所有数据。
    • 核心理念:
      release
      操作是“释放”了所有之前写入的可见性,
      acquire
      操作是“获取”了这些写入的可见性。
    std::atomic<int> data_item(0);
    std::atomic<bool> data_ready(false);
    
    void producer_thread() {
        data_item.store(100, std::memory_order_relaxed); // 写入数据
        data_ready.store(true, std::memory_order_release); // 释放,确保data_item可见
    }
    
    void consumer_thread() {
        while (!data_ready.load(std::memory_order_acquire)) { // 获取,等待data_ready为true
            std::this_thread::yield();
        }
        // 此时,data_item.load()保证能看到100
        std::cout << "Consumed: " << data_item.load(std::memory_order_relaxed) << std::endl;
    }
  3. std::memory_order_acq_rel
    :读-改-写操作的同步

    • 实际影响: 顾名思义,它结合了
      acquire
      release
      的语义。用于那些需要先读取一个值,然后根据读取的值进行修改,最后将新值写回的原子操作(如
      fetch_add
      compare_exchange
      )。它确保了读取操作能看到之前所有线程的写入,并且它本身的写入操作也能对后续的
      acquire
      操作可见。
    • 选择场景: 需要原子地更新一个共享变量,并且这个更新操作本身需要同步。例如,一个原子计数器,在更新时需要确保看到最新的值,并且更新后的值也要及时对其他线程可见。
    • 例子:
      counter.fetch_add(1, std::memory_order_acq_rel);
      这种操作在内部会先读取
      counter
      的当前值(
      acquire
      语义),然后加1,再将新值写回(
      release
      语义)。
  4. std::memory_order_seq_cst
    :最强保证,最易理解,但最慢

    • 实际影响: 提供最强的内存排序保证,所有使用
      seq_cst
      的原子操作都会在一个全局的、唯一的总序中被观察到。这意味着,所有线程对
      seq_cst
      操作的观察顺序都是一致的,并且所有
      seq_cst
      操作都不能被重排。
    • 选择场景: 当你对内存顺序有疑问,或者需要最简单、最直观的并发行为时,
      seq_cst
      是你的首选。它能有效避免各种复杂的内存重排问题,让代码行为更可预测。
    • 代价: 这种强保证通常伴随着最高的性能开销,因为它可能需要更多的CPU指令或内存屏障来强制排序。
    • 默认值: C++标准中,所有
      std::atomic
      操作的默认内存顺序都是
      seq_cst
      。如果你不指定,就是它在起作用。
  5. std::memory_order_consume
    :一个复杂且不常用的选项

    • 实际影响: 它比
      acquire
      弱,只对那些“数据依赖”于
      consume
      加载值的后续操作提供排序保证。例如,如果你加载了一个指针,然后通过这个指针访问数据,
      consume
      能保证指针指向的数据是可见的。
    • 选择场景: 理论上可以提供比
      acquire
      更好的性能,但其语义非常复杂,难以正确使用,且编译器实现上往往直接将其提升为
      acquire
      ,导致其性能优势不明显。
    • 建议: 如果不是对C++内存模型有极其深入的理解,并且有明确的性能瓶颈需要优化,通常建议避免使用
      consume
      ,而直接使用
      acquire

选择正确的

memory_order
,本质上是在正确性和性能之间做权衡。从最强的
seq_cst
开始,如果发现性能瓶颈,再逐步尝试放宽到
acq_rel
acquire/release
,甚至是
relaxed
,但每一步都需要经过严谨的测试和分析,以确保没有引入新的并发问题。这需要对并发编程有深刻的理解和实践经验。

避免常见陷阱:原子操作与内存顺序的错误使用分析

在我看来,原子操作和内存顺序就像是精密的手术刀,用好了能切中要害,效率奇高;用不好,就可能伤及无辜,甚至导致整个系统崩溃。这里列举一些我在实践中遇到或观察到的常见陷阱:

  1. 忘记原子性:将

    std::atomic
    当作普通变量使用 这是一个非常常见的初学者错误。
    std::atomic<int> value;
    声明了一个原子整数,但如果你直接写
    int temp = value;
    或者
    value = temp + 1;
    ,而不是使用
    value.load()
    value.store()
    value.fetch_add()
    等成员函数,那么这些操作的原子性就可能无法保证。虽然现代编译器在某些简单场景下可能会“魔法般地”让这些操作看起来是原子的(尤其是对于像
    int
    这样的小类型),但这并不是标准保证的行为,并且会让你失去控制内存顺序的能力。

    • 正确做法: 始终通过
      std::atomic
      提供的成员函数来访问和修改原子变量。
  2. memory_order_relaxed
    的滥用
    relaxed
    是最宽松的内存顺序,性能最好,但也最危险。很多人看到“性能最好”就想用它,但却忽略了它不提供任何跨线程的排序保证。

    • 陷阱: 假设一个线程写入了两个
      relaxed
      原子变量
      A
      B
      ,另一个线程读取
      A
      B
      。即使写入线程先写入
      A
      再写入
      B
      ,读取线程也可能先看到
      B
      的更新,然后才看到
      A
      的更新,甚至永远看不到
      A
      的更新,如果
      A
      B
      之间存在隐式的数据依赖,这就会导致程序逻辑错误。
    • 示例: 生产者写入数据
      data
      ,然后设置
      flag
      为true。消费者等待
      flag
      为true后读取
      data
      。如果
      data
      flag
      都用
      relaxed
      ,消费者可能在
      flag
      为true后,仍然读到旧的
      data
      值。
    • 避免: 除非你明确知道你的操作不需要任何排序,并且已经仔细分析过所有潜在的并发路径,否则不要轻易使用
      relaxed
  3. 误解

    acquire/release
    的同步范围
    acquire/release
    语义是C++内存模型中非常强大的工具,但它的同步范围常常被误解。

    • 陷阱:
      release
      操作只保证其之前的写入对匹配的
      acquire
      操作可见。它不保证
      release
      操作之后的写入会提前对其他线程可见。同样,
      acquire
      操作只保证其之后的读取能看到匹配
      release
      操作之前的写入。它不保证
      acquire
      操作之前的读取能看到

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1031

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

612

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

334

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

235

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

549

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

44

2026.01.06

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

37

2026.03.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 10.6万人学习

C 教程
C 教程

共75课时 | 5.4万人学习

TypeScript全面解读课程
TypeScript全面解读课程

共26课时 | 5.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号