C++八股 —— 原子操作

发布于:2025-05-25 ⋅ 阅读:(16) ⋅ 点赞:(0)

参考

1. 什么是原子操作

原子操作(Atomic Operations)是不可分割的操作,保证在多线程环境中执行时不会被中断,避免数据竞争(Data Race)。即:原子操作要么完全执行,要么完全不执行,不会在执行期间被其他线程操作干扰。

C++11引入<atomic>头文件,提供std::atomic<T>模板类,支持对基本类型(如intbool、指针)的原子操作。

2. 原子操作的特点

  • 无锁(Lock-Free): 原子操作通常由硬件指令(如CAS,LL/SC)实现,无需显式加锁。
  • 线程安全: 保证对变量的读写操作在多线程环境下的正确性。
  • 轻量级: 适合简单操作(如计数器、标志位),性能通常高于互斥锁。

示例:

#include <atomic>
std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
}

3. 原子操作的底层原理

原子操作的实现依赖于硬件指令,例如:

  • CAS(Compare-And-Swap):通过比较内存中的值与预期值,若匹配则更新为新值。

    bool compare_exchange_strong(T& expected, T desired) {
        if (current == expected) {
            current = desired;
            return true;
        } else {
            expected = current;
            return false;
        }
    }
    
  • LL/SC(Load-Linked/Store-Conditional):在特定地址上标记“监视”,若未被其他线程修改则写入。

这些指令保证操作的原子性,但不同硬件(x86、ARM)的实现差异较大。例如:

  • x86:通过LOCK前缀指令(如LOCK XCHG)实现原子操作。
  • ARM:依赖LL/SC指令实现无锁原子操作。

原子操作的局限性

  • ABA问题:线程A读取值A,线程B将值改为B后又改回A,导致A的CAS误判无变化。
    解决方案:使用带版本号的原子变量(如std::atomic<std::pair<T, uint64_t>>)。
  • 适用范围:仅适用于基本类型(如intbool、指针)。复杂类型需使用std::atomic_flag或互斥锁。

4. 内存序

内存序(Memory Order)定义了原子操作之间的可见性顺序约束,确保多线程环境下的内存访问符合预期。以下问题可能导致指令顺序变化:

  • 编译器重排:编译器优化可能导致指令顺序变化。
  • CPU重排:现代CPU的乱序执行机制(如Store Buffer、Invalidate Queue)可能打乱指令顺序。
  • 缓存一致性:不同核心的缓存状态可能导致内存视图不一致。

C++提供6种内存序,分为三类:

内存序 描述
顺序一致性(Sequential Consistency)
memory_order_seq_cst 最严格,保证全局顺序一致,性能较低。默认选项。
获取-释放(Acquire-Release)
memory_order_acquire 保证后续读操作不会重排到该操作之前(用于“获取”同步)。
memory_order_release 保证前面的写操作不会重排到该操作之后(用于“释放”同步)。
memory_order_acq_rel 同时包含acquire和release语义(用于读-改-写操作)。
宽松(Relaxed)
memory_order_relaxed 仅保证原子性,无顺序约束(适用于计数器等无需同步的场景)。
memory_order_consume 依赖顺序(较弱的acquire,C++17后不推荐使用)。

典型场景:

生产者-消费者模型:

// 生产者线程
data = ...; // 生产数据
flag.store(true, std::memory_order_release); // 发布数据

// 消费者线程
while (!flag.load(std::memory_order_acquire)); // 获取数据
use_data(data); // 安全使用数据

内存屏障

1. 内存屏障的作用

内存屏障是硬件或编译器级别的指令,用于强制限制内存操作的执行顺序,分为两类:

  • 编译器屏障:阻止编译器重排指令(如asm volatile("" ::: "memory"))。
  • 硬件屏障:阻止CPU重排内存访问(如x86的MFENCE、ARM的DMB)。

2. C++内存序与内存屏障的映射

  • std::atomic_thread_fence():显式插入内存屏障。

    std::atomic_thread_fence(std::memory_order_acquire); // 插入读屏障
    
  • 隐式屏障:原子操作的内存序参数隐式插入屏障:

    a.store(1, std::memory_order_release); // 隐式插入Store屏障
    

3. 内存屏障的实际案例

示例:无锁队列的入队操作

// 生产者线程
Node* new_node = new Node(data);
new_node->next.store(head, std::memory_order_relaxed);
head.store(new_node, std::memory_order_release); // 插入写屏障,确保new_node初始化完成后再更新head

// 消费者线程
Node* local_head = head.load(std::memory_order_acquire); // 插入读屏障,确保读取到最新的head
if (local_head != nullptr) {
    // 安全操作local_head->next
}

5. 原子操作和互斥锁的对比

特性 原子操作 互斥锁(如std::mutex)
实现方式 硬件指令(如CAS)实现无锁操作。 通过操作系统内核的锁机制(可能涉及上下文切换)。
性能 低竞争时性能高,高竞争时可能自旋。 高竞争时可能更高效(线程休眠)。
适用场景 简单操作(如计数器、标志位)。 复杂操作或需要保护多个变量/代码块。
内存序复杂性 需显式指定内存序,易出错。 隐式保证顺序一致性,更简单。
死锁风险 无。 需避免死锁(如加锁顺序不一致)。
ABA问题 可能发生(需配合版本号解决)。 无。

选择建议:

  • 使用原子操作:需要高性能的简单操作,且能正确处理内存序。
  • 使用互斥锁:保护复杂逻辑或临界区较长时,简化代码并减少错误。

6. 常用的原子操作

  • load:读取原子变量的值

    std::atomic<int> a(1);
    int value = a.load();
    
  • store:将一个值存储到原子变量中

    std::atomic<int> a(1);
    a.store(2); // a的值变为2
    
  • exchange:将原子变量的值替换为另外一个值,并返回旧值

    常用来在并发环境下进行“交换”操作。

    std::atomic<int> a(1);
    int old_value = a.exchange(2); // old_value为1,a的值变为2
    
  • compare_exchange_weak / compare_exchange_strong:原子地进行条件交换操作。若当前值等于预期值,则交换新值,否则返回falseweak失败后可能会重新尝试,性能相对较高;strong失败后会返回false并不再尝试。

    std::atomic<int> a(1);
    int expected = 1;
    if (a.compare_exchange_weak(expected, 2)) {
        // 如果a的值是1,设置为10
        std::cout << "Value changed!" << std::endl;
    } else {
        std::cout << "Value not changed!" << std::endl;
    }
    
  • fetch_add / fetch_sub:原子地执行加法或减法操作,并返回旧值。

    std::atomic<int> a(5);
    int old_value = a.fetch_add(1);  // old_value为5,a为6
    

7. 相关问题讨论


网站公告

今日签到

点亮在社区的每一天
去签到