C++内存序的操作方法

 更新时间:2025年11月04日 09:29:54   作者:2301_80355452  
在C++中,内存序(Memory Order)是一个非常重要的概念,特别是在多线程编程中,本文通过实例代码介绍C++内存序的相关知识,感兴趣的朋友一起看看吧

1.为什么需要内存序?

问题的根源:现代CPU的乱序执行

// 从程序员角度看是顺序执行
int x = 0, y = 0;
void thread1() {
    x = 1;  // 步骤1
    y = 1;  // 步骤2
}
void thread2() {
    if (y == 1) {
        assert(x == 1);  // 可能失败!
    }
}

实际执行可能:

  • CPU为了优化,可能先执行 y = 1,后执行 x = 1
  • 编译器也可能重排指令
  • 导致线程2看到 y == 1 但 x == 0

2.六种内存序详解

内存序概览表

内存序作用使用场景性能
relaxed只保证原子性计数器、统计最快
consume数据依赖排序指针发布较快
acquire加载屏障读侧同步中等
release存储屏障写侧同步中等
acq_rel加载+存储屏障CAS操作较慢
seq_cst全序屏障默认,最安全最慢

3.逐层深入理解

3.1memory_order_relaxed- 最宽松

#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> x(0), y(0);
void relaxed_example() {
    // 线程1
    std::thread t1([](){
        x.store(1, std::memory_order_relaxed);  // 可能重排
        y.store(1, std::memory_order_relaxed);  // 可能重排
    });
    // 线程2
    std::thread t2([](){
        int r1 = y.load(std::memory_order_relaxed);  // 可能看到y=1但x=0
        int r2 = x.load(std::memory_order_relaxed);
        std::cout << "y=" << r1 << ", x=" << r2 << std::endl;
    });
    t1.join(); t2.join();
}

适用场景:

// 计数器 - 顺序不重要,只要原子就行
std::atomic<int> counter(0);
void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

3.2memory_order_acquire和memory_order_release- 配对使用

#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> flag{false};
int data = 0;
void producer() {
    data = 42;                                  // 1. 准备数据
    flag.store(true, std::memory_order_release); // 2. 发布标志(保证1在2之前)
}
void consumer() {
    while (!flag.load(std::memory_order_acquire)) { // 3. 获取标志(保证4在3之后)
        // 等待
    }
    assert(data == 42);  // 4. 读取数据(这里一定能看到42!)
}

屏障效果图示:

线程A (Producer)        线程B (Consumer)
data = 42
    ↓ (release屏障)
flag = true
                ─────→   while(!flag)
                            ↓ (acquire屏障)
                         assert(data==42) ✓

3.3memory_order_acq_rel- 读改写操作

class SpinLock {
    std::atomic<bool> locked{false};
public:
    void lock() {
        // 期望locked=false,设置locked=true
        // 需要同时保证acquire和release语义
        while (locked.exchange(true, std::memory_order_acq_rel)) {
            // 自旋等待
        }
    }
    void unlock() {
        locked.store(false, std::memory_order_release);
    }
};

为什么需要acq_rel:

std::atomic<int> shared{0};
int normal_data = 0;
void thread_work() {
    // 修改前操作
    normal_data = 100;
    // CAS操作:既有读又有写
    int expected = 0;
    if (shared.compare_exchange_strong(expected, 1, 
                                      std::memory_order_acq_rel)) {
        // 成功:相当于acquire,能看到之前的修改
        // 同时:相当于release,保证之前的修改对其他线程可见
    }
}

3.4memory_order_seq_cst- 顺序一致性(默认)

std::atomic<int> x{0}, y{0};
void sequential_example() {
    std::thread t1([](){
        x.store(1, std::memory_order_seq_cst);  // 1
        y.store(1, std::memory_order_seq_cst);  // 2
    });
    std::thread t2([](){
        int r1 = y.load(std::memory_order_seq_cst);  // 3
        int r2 = x.load(std::memory_order_seq_cst);  // 4
        // 所有线程看到相同的操作顺序
        // 可能的顺序:1→2→3→4 或 1→3→2→4 等
        // 但所有线程对顺序的理解一致
    });
    t1.join(); t2.join();
}

4.在shared_ptr中的实际应用

您代码中的内存序分析:

void release() {
    if (ref_count && ref_count->fetch_sub(1, std::memory_order_acq_rel) == 1) {
        delete ptr;
        delete ref_count;
    }
}
// 为什么用 memory_order_acq_rel?

详细解释:

class shared_ptr {
    void release() {
        // fetch_sub是读-修改-写操作:
        // 1. 读取当前值 (需要acquire语义)
        // 2. 减去1
        // 3. 写回新值 (需要release语义)
        // 使用acq_rel确保:
        if (ref_count && 
            ref_count->fetch_sub(1, std::memory_order_acq_rel) == 1) {
            // ↓ 这个delete操作必须看到ptr的所有修改
            delete ptr;     // 需要acquire保证看到完整对象状态
            delete ref_count;
            // 同时,在delete之前的所有对象修改
            // 必须对其他线程可见 (release语义)
        }
    }
};

5.内存序的层次关系

从弱到强的约束:

relaxed (最弱)
   ↓
consume
   ↓  
acquire/release
   ↓
acq_rel
   ↓
seq_cst (最强)

6.实际编程建议

6.1什么时候用什么内存序

// 情况1:简单计数器
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);  // ✅
// 情况2:标志位同步
std::atomic<bool> ready{false};
int data;
// 生产者
data = compute_data();
ready.store(true, std::memory_order_release);     // ✅
// 消费者  
while (!ready.load(std::memory_order_acquire)) {} // ✅
use_data(data);
// 情况3:复杂的同步逻辑(不确定时)
std::atomic<int> state{0};
state.compare_exchange_strong(old_val, new_val, 
                             std::memory_order_seq_cst);  // ✅ 安全第一

6.2实用示例:无锁队列

template<typename T>
class LockFreeQueue {
    struct Node {
        T data;
        std::atomic<Node*> next;
    };
    std::atomic<Node*> head, tail;
public:
    void push(const T& value) {
        Node* new_node = new Node{value, nullptr};
        Node* old_tail = tail.load(std::memory_order_acquire);
        while (!tail.compare_exchange_weak(old_tail, new_node,
                                          std::memory_order_acq_rel,
                                          std::memory_order_acquire)) {
            // CAS失败,重试
        }
        // 链接节点
        old_tail->next.store(new_node, std::memory_order_release);
    }
};

7.测试和验证

内存序错误的检测:

#include <atomic>
#include <thread>
#include <cassert>
std::atomic<int> x{0}, y{0};
int r1, r2;
void memory_order_test() {
    std::thread t1([](){
        x.store(1, std::memory_order_relaxed);
        y.store(1, std::memory_order_relaxed);
    });
    std::thread t2([](){
        r1 = y.load(std::memory_order_relaxed);
        r2 = x.load(std::memory_order_relaxed);
    });
    t1.join(); t2.join();
    // 在relaxed顺序下,可能发生:
    // r1 == 1 (y已设置) 但 r2 == 0 (x还未设置)
    std::cout << "Result: r1=" << r1 << ", r2=" << r2 << std::endl;
}

8.总结

关键要点:

  • relaxed:只保证原子性,不保证顺序
  • acquire/release:配对使用,建立线程间happens-before关系
  • acq_rel:用于读-修改-写操作,兼具两者特性
  • seq_cst:全局顺序,最安全但性能最差

使用建议:

  • 初学者:先用 seq_cst,确保正确性
  • 性能优化:在理解的基础上使用更宽松的内存序
  • shared_ptr场景acq_rel 是正确的选择
  • 标志同步acquire/release 是最佳选择

您的shared_ptr实现中使用的 memory_order_acq_rel 是非常合适的,它确保了在释放资源时能够看到对象的完整状态,同时也保证了对象修改对其他线程的可见性。

到此这篇关于C++内存序的操作方法的文章就介绍到这了,更多相关C++内存序内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • VS2022添加代码模板的实现步骤(图文)

    VS2022添加代码模板的实现步骤(图文)

    使用代码模板即可实现像内置函数那样,只需写几个字母,便能提示自动补全,本文主要介绍了VS2022添加代码模板的实现步骤,感兴趣的可以了解一下
    2024-06-06
  • 一文带你了解C++中的字符替换方法

    一文带你了解C++中的字符替换方法

    这篇文章主要为大家详细介绍了C++中常用的几个字符替换方法,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以跟随小编一起了解一下
    2023-04-04
  • 关于C语言strlen与sizeof区别详情

    关于C语言strlen与sizeof区别详情

    对于 strlen 和 sizeof,相信不少程序员会混淆其功能。虽然从表面上看它们都可以求字符串的长度,但二者却存在着许多不同之处及本质区别,今天得这篇文章我们就来学习C语言strlen与sizeof区别的相关资料,需要的朋友可以参考一下
    2021-10-10
  • MATLAB中count函数用法示例代码

    MATLAB中count函数用法示例代码

    这篇文章主要介绍了MATLAB中的count函数,用于计算字符串中模式的出现次数,它可以处理单个模式和模式数组,并且可以忽略大小写,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-02-02
  • C++中运算符重载详解及其作用介绍

    C++中运算符重载详解及其作用介绍

    这篇文章主要介绍了C++中运算符重载详解及其作用介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • c++ 成员函数与非成员函数的抉择

    c++ 成员函数与非成员函数的抉择

    尽量用类的非成员函数以及友元函数替换类的成员函数 例如一个类来模拟人People
    2013-01-01
  • 浅谈c++ 预处理器

    浅谈c++ 预处理器

    这篇文章主要介绍了c++ 预处理器的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下
    2020-06-06
  • c语言全盘搜索指定文件的实例代码

    c语言全盘搜索指定文件的实例代码

    c语言全盘搜索指定文件的实例代码,需要的朋友可以参考一下
    2013-03-03
  • 详解C++编程中向函数传递引用参数的用法

    详解C++编程中向函数传递引用参数的用法

    这篇文章主要介绍了详解C++编程中向函数传递引用参数的用法,包括使函数返回引用类型以及对指针的引用,需要的朋友可以参考下
    2016-01-01
  • QT实现年会抽奖小软件的示例代码

    QT实现年会抽奖小软件的示例代码

    本文主要介绍了QT实现年会抽奖小软件的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01

最新评论