展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > 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
    作者最新文章
    编程开发
    相关文章 更多
    精品专题 更多
    装机必备

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

    Windows

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

    PDF教程

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

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版

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

    火山引擎 网页版
    火山引擎 网页版

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

    Aperty macOS版
    Aperty macOS版

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

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版

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

    火山引擎 网页版
    火山引擎 网页版

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

    PixTV 网页版
    PixTV 网页版

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

    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

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

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。