当前位置:

首页 > 编程开发 > C++ 内存序六件套:从完全同步到爱咋咋地

C++ 内存序六件套:从完全同步到爱咋咋地

C++原子操作的内存序从最严格的顺序一致性到最宽松的自由序,不同级别控制着指令重排与可见性。使用恰当可提升性能,选错则破坏数据一致性,需明确每种内存序的适用场景。

本文目录

    我们写了个 store(memory_order_relaxed),CPU 心想“好的老板,我先把后面那行提前执行了,因为它效率高”,然后我们的数据就烂了。

    C++ 内存序六件套:从完全同步到爱咋咋地

    C++ 的这几种内存序用好了起飞,用错了也能起飞。关键在于,得清楚每一把“钥匙”到底能打开哪扇门。

    原子操作与内存序枚举

    1. std::atomic 的常用操作

    单纯的 load / store

    std::atomic<int> flag{ 0 };
    int a = flag.load(); // 读
    flag.store(1); // 写
    

    load 负责原子地获取原子对象的值,store 则是原子地将值替换成非原子参数。它们的内存序默认为 memory_order_seq_cst,也就是最严格、最安全但也是性能开销最大的那个。

    读-改-写全家桶

    • exchange():原子地替换值并返回旧值。可以理解为一个“交换”操作,既能拿回旧信息,又能设定新状态。
    int main()
    {
        std::atomic<int> flag(0);
        int old = flag.exchange(1); // 原子地设为1,返回旧值0
        std::cout << "旧值: " << old << ", 新值: " << flag.load() << std::endl;
        // 输出: 旧值: 0, 新值: 1
    }
    
    • compare_exchange_weak() / compare_exchange_strong():经典的“比较并交换”。只有当前值等于期望值时,才将其替换成新值,否则将当前值写入期望值。返回值指示是否替换成功。

    weak 版本在某些平台上性能更好,但它可能出现“虚假失败”——明明值相等却告诉你不行。因此,它必须在循环中使用,不断重试。strong 版本则保证:只有值真正不同时才会失败,使用起来更直观。

    std::atomic<int> counter(0);void increment()
    {
        int expected = counter.load();
        while (!counter.compare_exchange_weak(expected, expected + 1))
        {
            // 失败时 expected 已被更新为当前值,再次尝试
        }
    }int main()
    {
        std::vector threads;
        for (int i = 0; i < 10; ++i)
            threads.emplace_back(increment);
        for (auto& t : threads)
            t.join();
        std::cout << "最终计数: " << counter << std::endl; // 10
    }
    
    • fetch_add() / fetch_sub():对整数或指针原子地执行加/减操作,并返回旧值。这是实现无锁计数器的核心。
    std::atomic<int> total(0);void add(int n)
    {
        for (int i = 0; i < 1000; ++i)
            total.fetch_add(n);
    }int main()
    {
        std::vector threads;
        for (int i = 0; i < 4; ++i)
            threads.emplace_back(add, 1);
        for (auto& t : threads)
            t.join();
        std::cout << "总和: " << total << std::endl; // 4000
    }
    

    2. 内存序六元组

    C++ 一口气给了我们六个枚举值,按实际能力可以划分成几个层次。

    seq_cst:顺序一致性

    memory_order_seq_cst 是默认选项,也是唯一一个能给所有 atomic 操作建立全局一致修改顺序的选项。简单来说,整个程序里所有原子变量的操作,就像在一个看不见的队列上排队发生。我们能清楚地讲出“A 的 store 在 B 的 load 之前”这种话,而且这个顺序对所有线程可见、一致。

    代价呢?在现代 CPU 上,它经常要插入昂贵的内存栅栏。用它写生产者消费者模型,稳是稳,但性能浪费了一大半,属于用劳斯莱斯的成本干三轮车的活。

    release / acquire / acq_rel:高效同步

    • store(memory_order_release):当前线程在此之前的所有写操作,对于之后对此原子变量做获取读的线程,全部可见。
    • load(memory_order_acquire):当前线程在此之后的所有读/写,不会被重排到这个 load 之前。
    • memory_order_acq_rel 只用于“读-改-写”操作,兼具 acquire 和 release 的效果,是一个“读-改-写全局同步点”。

    relaxed:只有原子性,没有顺序

    memory_order_relaxed 是真正的“没有感情的计数器”。它只保证操作本身是原子的,至于操作在全局顺序里排哪、其他线程能不能看到其他变量的最新值,一概不管。

    它的绝对安全区在哪?就是那些无同步依赖的独立计数器:比如统计访问次数、引用计数的加加减减,只要最终一致,中间漏几个计数也无伤大雅。

    一旦试图用 relaxed 去做标记保护数据,就会喜提“编译器/CPU 随便重排,数据未初始化”的经典体验。

    consume:坑

    memory_order_consume 设计初衷是比 acquire 更轻量,只保证数据依赖关系有序。但现实是,几乎所有编译器都把它当作 acquire 来实现,因为正确实现依赖追踪太难了。标准委员会自己都快放弃了,至今处于不建议使用状态,基本可以当作不存在。

    逐项解析六种内存序

    1. memory_order_seq_cst

    seq_cst 帮我们把所有事情都安排好了,很安全,但后来才发现这份“保护费”有多昂贵。

    它干了什么?所有标记 seq_cst 的操作,在所有线程看来,都遵守同一个全局的、单一的总顺序。不用担心“线程 A 看到的顺序和线程 B 不一样”,所有 seq_cst 操作就像排队,谁先谁后一目了然,而且对所有观察者一致。

    std::atomic<bool> x{false}, y{false};
    std::atomic<int> z{0};void write_x()
    {
        x.store(true, std::memory_order_seq_cst);
    }void write_y()
    {
        y.store(true, std::memory_order_seq_cst);
    }void read_x_then_y()
    {
        while (!x.load(std::memory_order_seq_cst)) {}
        if (y.load(std::memory_order_seq_cst))
            ++z;
    }void read_y_then_x()
    {
        while (!y.load(std::memory_order_seq_cst)) {}
        if (x.load(std::memory_order_seq_cst))
            ++z;
    }int main()
    {
        std::thread t1(write_x), t2(write_y), t3(read_x_then_y), t4(read_y_then_x);
        t1.join(); t2.join(); t3.join(); t4.join();
        assert(z.load() != 0); // 永远不会失败
    }
    

    这个例子是经典的商店-客户条件。用 seq_cst 时 z 永远不会为 0,因为 seq_cst 保证全局一致性:没有任何观察者会看到 x 和 y 都已被置位却漏掉另一个。换成更弱的内存序,这个断言就敢崩给我们看。

    seq_cst 默认省心,但在 x86 上其实也贵。x86 的 load 自带 acquire,store 自带 release,但加上 seq_cst 后每次都得多等一个锁总线,性能可能直接腰斩。

    2. acquire 和 release

    它们不是锁,却比锁更灵活,专门用来在写线程与读线程之间传递“前面那一堆数据都就绪了”的信号。

    • store(memory_order_release):当前线程在此之前的所有写操作,对于之后对此原子变量做获取读的线程,全部可见。
    • load(memory_order_acquire):当前线程在此之后的所有读/写,不会被重排到这个 load 之前。

    有一点必须牢记:release 和 acquire 必须作用于同一个原子变量才能直接建立同步关系。换到不同变量上,就没有这种效果。

    std::atomic<bool> ready{ false };
    std::string data; // 普通共享变量void producer()
    {
        data = "非常非常重要的消息"; // 1
        ready.store(true, std::memory_order_release); // 2、release
    }void consumer()
    {
        while (!ready.load(std::memory_order_acquire)) {} // 3、acquire!
        assert(data == "非常非常重要的消息"); // 4、保证看见 1
    }
    

    当 consumer 看到 ready 为 true 时,它也同时保证看到了 data 的完整写入。这就是经典的 happens-before 链:1 → 2(同线程顺序),2 与 3 之间形成同步,3 → 4(同线程顺序)。没有这个序,大概率会在 data 里读到空字符串或半条乱码。

    3. memory_order_acq_rel

    这是给读-改-写操作(RMW)量身定做的,相当于在同一个操作里既做了 acquire 又做了 release。

    典型场景是:用原子变量实现自旋锁、或节点引用计数调整时,需要同时建立前后因果关系。

    std::atomic<int> sync_point{ 0 };void thread1()
    {
        int old = sync_point.fetch_add(1, std::memory_order_acq_rel);
        // 获取之前的状态 + 释放本次修改给后面的观察者
    }
    

    更具体的一个自旋锁:

    std::atomic<bool> lock_flag{false};void lock()
    {
        while (lock_flag.exchange(true, std::memory_order_acq_rel))
        {
            // 自旋等待
        }
        // 获取锁:acquire 部分确保临界区不会泄漏到加锁前
        // 释放的语义则由之后显式 store(release) 负责,其实用 acquire 就够了,acq_rel 在这里稍重,但无伤大雅
    }
    

    不过说实话,自旋锁的标准写法通常用 acquire 获取,release 释放。而 RMW 里用 acq_rel,主要是为了某些复杂无锁结构,必须一步完成 acquire 和 release,防止中间被插队。

    acq_rel 用起来要斟酌:它比单纯的 release 或 acquire 都重,如果只是要读或只是写,别上头乱套。

    4. memory_order_consume

    consume 的设计初衷是轻量级 acquire:只保证依赖于这个 load 结果的数据依赖关系有序,而不必像 acquire 那样建立一个沉重的栅栏。比如:

    std::atomic ptr;
    std::string data;void producer()
    {
        data = "message";
        ptr.store(&data, std::memory_order_release);
    }void consumer()
    {
        std::string* p = ptr.load(std::memory_order_consume);
        if (p)
        {
            // 因为有依赖,*p 能看到完整的 "message"
            assert(*p == "message");
        }
    }
    

    理论上很美好,利用 CPU 内部的数据依赖顺序,省去一道内存屏障。但问题来了:编译器要精确追踪依赖,太难了。什么 p->length()、把指针放进结构体再转出来、函数调用间接引用……编译器几乎没法在不产生大量保守同步的情况下证明没有依赖。于是,主流编译器直接摆烂,把 consume 实现为完整的 acquire。

    这就导致一个荒诞局面:你写 consume 本来想省性能,结果编译器默默给你加了全套 acquire 的栅栏,还多了语义不一致的风险。标准委员会也基本放弃了,当前明确定位为不鼓励使用。

    5. memory_order_relaxed

    relaxed 只做一个承诺:对该变量的操作本身是原子的,仅此而已。没有 happens-before,没有全局顺序,连编译器重排都放任自由。

    合法用法:不需要任何同步副作用的纯计数器。

    std::atomic<long long> hit_count{0};void increment()
    {
        hit_count.fetch_add(1, std::memory_order_relaxed);
    }
    

    就算两个线程同时加,得到的一定是正确加法结果(原子性),但此时其他变量可能乱七八糟,根本无法推断除了这个计数器本身之外还有什么值。这在统计请求数、错误计数等场景完全够用。

    作死示范:

    std::atomic<bool> valid{ false };
    int value = 0;void threadA()
    {
        value = 21;
        valid.store(true, std::memory_order_relaxed);  // 1、没有 release
    }void threadB()
    {
        if (valid.load(std::memory_order_relaxed)) // 2、没有 acquire
        {   
            // 可能 value 还是 0,因为顺序未定义
            std::cout << value << std::endl;
        }
    }
    

    这段代码在一些 CPU 和编译器眼里,完全可以把 valid.store 提前到 value = 21 之前,把混乱展示得淋漓尽致。

    6. 总结一下

    • 想要安全感,用 seq_cst。
    • 想要性能又要有正确同步,release/acquire 是绝对主力。
    • 只做纯原子运算且不涉及任何数据依赖,relaxed 随便用。
    • acq_rel 留给 RMW 的复合需求。
    • consume?请直接烧纸上香,忘掉它。

    内存序的典型应用模式

    1. 无锁单生产者单消费者队列

    单生产者单消费者意味着不需要处理多写者的复杂竞争,只需要防止生产者和消费者踩同一个地方。

    一个简单的环形缓冲区,用两个 atomic 下标 + relaxed + release/acquire 就能搭起来:

    template <typename T, size_t N>
    class SPSCQueue
    {
        T buffer[N];
        std::atomic<size_t> write_idx{ 0 };
        std::atomic<size_t> read_idx{ 0 };
    public:
        bool try_push(const T& item)
        {
            size_t w = write_idx.load(std::memory_order_relaxed);
            size_t next = (w + 1) % N;
            if (next == read_idx.load(std::memory_order_acquire)) // 获取消费者最新进度
                return false; // 满
            buffer[w] = item; // 1
            write_idx.store(next, std::memory_order_release); // 2 发布元素
            return true;
        }    bool try_pop(T& item)
        {
            size_t r = read_idx.load(std::memory_order_relaxed);
            if (r == write_idx.load(std::memory_order_acquire)) // 获取生产者最新进度
                return false; // 空
            item = buffer[r]; // 3
            read_idx.store((r + 1) % N, std::memory_order_release); // 4 释放槽位
            return true;
        }
    };
    

    这里的要点在于:

    • 生产者用 relaxed 读自己的 write_idx 没问题,因为只有自己在改它。但读 read_idx 必须用 acquire,这样能看到消费者刚释放的槽位确实是空的。
    • 消费者对称:读 write_idx 用 acquire 可以看到生产者刚写入的元素,读自己的 read_idx 用 relaxed。

    如果这里用 seq_cst,会强制额外同步,把这条轻量队列的性能优势吃掉一大半。

    2. 双层检查锁定的实现

    C++ 史上最臭名昭著的反模式之一,直到 atomic 和正确的内存序入场才得以洗净冤屈。

    需求:懒汉单例,首次构造后无锁访问。

    早期作死写法:

    if (!instance) // 非原子读取,数据竞争!
    {
        lock_guard lock(mtx);
        if (!instance) instance = new Foo; // 普通指针,可能指令重排
    }
    

    这等于把未定义拌上数据竞争再浇一勺指令重排,味道相当“醇厚”。

    现在使用原子 + acquire/release:

    class Foo
    {
    public:
        void doSomething() { /* 实际业务逻辑 */ }private:
        Foo() = default;    // 禁止拷贝和移动,保证单例唯一性
        Foo(const Foo&) = delete;
        Foo& operator=(const Foo&) = delete;    // 设为友元,使其能访问私有构造函数
        friend Foo* getFoo();
    };std::atomic instance{ nullptr };
    std::mutex mtx;Foo* getFoo()
    {
        Foo* tmp = instance.load(std::memory_order_acquire); // 1
        if (!tmp)
        {
            std::lock_guard lock(mtx);
            tmp = instance.load(std::memory_order_relaxed); // 2 持锁后再看一次
            if (!tmp)
            {
                tmp = new Foo();
                instance.store(tmp, std::memory_order_release); // 3
            }
        }
        return tmp;
    }
    

    这个方案里:

    • 1 用 acquire:一旦看见非空指针,保证能看到对象的完整构造。
    • 2 用 relaxed:在锁内第二次读取,没有竞争,不需要同步。
    • 3 用 release:对象构造完毕,发布给后续所有 acquire 读操作。

    3. 发布一系列对象(发布-订阅)

    这个模式本质就是 release / acquire 的扩大版本:生产的不止一个变量,而是一整包数据,然后一把发布出去;消费者用自己的订阅动作一手接住全部数据。

    一个典型例子:多线程日志后端,主线程把一帧完整日志数据填好,然后原子地发布指针,日志消费者看到指针就安全消费。

    struct LogBlock
    {
        char data[256];
        size_t len;
    };std::atomic latest_log{ nullptr };void producer(const char* msg)
    {
        auto* block = new LogBlock();
        strncpy(block->data, msg, sizeof(block->data));
        block->len = strlen(msg);
        // 所有数据准备好,一次性发布
        latest_log.store(block, std::memory_order_release);
    }void consumer()
    {
        while (true)
        {
            LogBlock* block = latest_log.load(std::memory_order_acquire);
            if (block)
            {
                // 拿到指针后,由于 acquire,看到的是完整的 block 内容
                write_to_disk(block->data, block->len);
                // 为了简化没回收内存
            }
        }
    }
    

    消息里所有普通成员(data、len)的写入,保证了在 store(release) 之前发生;消费者的 load(acquire) 则保证在这些成员的使用之前发生。这就形成了一个非常干净的同步链,不需要任何锁,发布者立刻回去干活,订阅者按自己的步调消费。

    这类场景要是不小心写成了 relaxed,就会喜提:数据不定时缺失、偶尔半条消息。

    结尾

    所以最后就一句话:std::atomic 就是一份使用说明书,内存序也不是玄学,是我们欠 CPU 的一份礼貌。忽略它,程序会跑,但跑着跑着容易摔进坑里。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    悟空浏览器怎么彻底清理浏览器配置文件
    悟空浏览器怎么彻底清理浏览器配置文件

    彻底清理悟空浏览器的数据,通常有三个路径:通过手机系统设置能一次性清除所有配置和用户信息;利用浏览器内置功能,可以只清理缓存而保留个性化设置;最彻底的则是卸载重装,让应用回到初始状态。 悟空浏览器用久了,是不是偶尔会感觉变慢,或者出现一些难以解释的小毛病?有时候,与其一个个排查问题,不如来一次彻底的

    漫蛙漫画官网首页登录入口
    漫蛙漫画官网首页登录入口

    漫蛙漫画首页登录入口https://www.manwamh.top/:一触即达的漫画世界核心枢纽 对于漫画迷而言,找到那个可靠的资源门户至关重要。漫蛙漫画的首页登录入口,正是这样一个核心渠道。通过它,你可以直接跳转到漫蛙2漫画APP的下载页面,轻松几步完成安装,马上就能投入到精彩纷呈的漫画之旅中。 漫蛙漫画首页登录入口: 官方入口地址已经为你备好

    漫蛙漫画官方入口
    漫蛙漫画官方入口

    漫蛙漫画官方入口与核心功能解析 寻找一个稳定、资源丰富的漫画在线阅读平台,是许多漫画爱好者的共同需求。其中,漫蛙漫画官方入口及其免费全集服务,受到了不少关注。那么,这个平台究竟如何?下面,我们就来深入了解一下它的具体信息与特色功能。 平台的官方访问地址是:https://www.manwa123.c

    漫蛙动漫APP最新版本下载
    漫蛙动漫APP最新版本下载

    对于动漫爱好者而言,找到一个资源全、体验佳的追番平台,常常是件头疼事。漫蛙动漫APP的定位很明确:就是要打造一个聚合全球正版动漫资源的专属乐园。无论是当季火爆的日漫新番、值得回味的经典国创,还是那些小众却精彩的优质动画,在这里基本都能一网打尽。平台底层采用了智能缓冲技术,最大程度保证了播放的流畅度,

    漫蛙官网入口免费下载最新版
    漫蛙官网入口免费下载最新版

    想要获取正版manwa漫蛙APP?官方指定下载渠道就是漫蛙官网入口。 对于漫画迷而言,找到一款心仪的阅读神器至关重要。漫蛙APP正是这样一款专为全球漫画爱好者打造的平台,它汇聚了从日韩人气连载到本土原创精品的海量资源。说它“应有尽有”毫不为过。更重要的是,其界面设计清爽直观,搭配智能检索功能,能让用

    漫蛙漫画在线观看入口
    漫蛙漫画在线观看入口

    想要畅享海量正版漫画资源?漫蛙漫画平台不容错过 对于漫画迷来说,找到一个资源丰富、体验流畅的正版平台实属幸事。漫蛙漫画凭借其优质的在线阅读服务,已经成为了许多资深读者的心头好。今天,我们就来详细拆解一下平台的访问方法与实用技巧,助你轻松开启漫画之旅。 漫蛙漫画网页入口页面指南 想立刻体验?官方在线观

    漫蛙如何下载最新版本
    漫蛙如何下载最新版本

    漫蛙漫画网APP官方下载方法全解析 对于资深漫画迷来说,找到一个资源全、更新快、体验佳的阅读平台,就像淘到了宝藏。漫蛙漫画网APP,正是这样一个专为爱好者打造的精品集散地。它汇聚了全球优质的正版漫画,从火爆的日漫连载到崛起的国创佳作,平台都能做到实时同步更新。当然,第一步得先把它装到设备上。接下来,

    漫蛙漫画APP最新版本下载
    漫蛙漫画APP最新版本下载

    漫蛙漫画APP:专业漫画阅读工具的全面革新 作为一款专业的漫画阅读工具,漫蛙漫画APP整合了全球优质漫画资源,其升级版漫蛙2在功能与体验上可谓全面革新。这款应用不仅囊括日漫经典与国漫新作,更通过简洁直观的界面设计和流畅操作,为用户打造沉浸式阅读环境,充分满足各类漫画爱好者的多样化需求。 漫蛙漫画特色

    卖茶叶妈妈3d漫蛙安卓版下载
    卖茶叶妈妈3d漫蛙安卓版下载

    当下动漫圈热议的卖茶叶妈妈3d漫蛙下载,凭借其突破性的3D视觉技术成为焦点 最近动漫爱好者们都在讨论一个现象级产品——卖茶叶妈妈3d漫蛙下载。这款由漫蛙漫画平台倾力打造的3D作品专区,用立体建模与动态分镜彻底颠覆了传统二维漫画的阅读模式。你猜怎么着?现在只需通过专区提供的便捷下载通道,无论是热血冒险

    漫蛙下载最新版本
    漫蛙下载最新版本

    漫蛙APP:资深漫画迷的移动图书馆与阅读利器 对于漫画爱好者来说,找到一个资源全、体验好的阅读平台,从来都不是件容易的事。而漫蛙APP,恰恰就是为这个需求而生的。它凭借海量的正版漫画库和极其便捷的离线下载功能,在圈内积累了相当不错的口碑。更重要的是,从经典的日本热血漫到新兴的国产原创作品,它几乎一网

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。