1. 为什么需要内存序?

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

cpp

// 从程序员角度看是顺序执行
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.1 memory_order_relaxed - 最宽松

cpp

#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();
}

适用场景:

cpp

// 计数器 - 顺序不重要,只要原子就行
std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

3.2 memory_order_acquire 和 memory_order_release - 配对使用

cpp

#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!)
}

屏障效果图示:

text

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

3.3 memory_order_acq_rel - 读改写操作

cpp

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:

cpp

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.4 memory_order_seq_cst - 顺序一致性(默认)

cpp

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中的实际应用

您代码中的内存序分析:

cpp

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?

详细解释:

cpp

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. 内存序的层次关系

从弱到强的约束:

text

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

6. 实际编程建议

6.1 什么时候用什么内存序

cpp

// 情况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 实用示例:无锁队列

cpp

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. 测试和验证

内存序错误的检测:

cpp

#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. 总结

关键要点:

  1. relaxed:只保证原子性,不保证顺序

  2. acquire/release:配对使用,建立线程间happens-before关系

  3. acq_rel:用于读-修改-写操作,兼具两者特性

  4. seq_cst:全局顺序,最安全但性能最差

使用建议:

  • 初学者:先用 seq_cst,确保正确性

  • 性能优化:在理解的基础上使用更宽松的内存序

  • shared_ptr场景acq_rel 是正确的选择

  • 标志同步acquire/release 是最佳选择

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐