目 录CONTENT

文章目录

C++原子操作:std::atomic

TalentQ
2025-07-28 / 0 评论 / 0 点赞 / 25 阅读 / 0 字

一、引言

在多线程编程中,数据竞争(data race)和原子性(atomicity)问题一直是开发者关注的重点。C++11 标准引入了 <atomic> 头文件,提供了类型安全的原子操作工具——std::atomic

二、原子性与数据竞争

2.1 什么是原子操作?

原子操作(Atomic Operation)指的是“不可分割”的操作,即在执行过程中不会被线程切换或中断。对于多线程环境下的共享变量,原子操作能有效防止数据竞争。

2.2 数据竞争的危害

数据竞争发生在两个或多个线程同时访问同一变量且至少有一个写操作时。如果没有合适的同步机制,结果是未定义的。

三、std::atomic 基本原理

3.1 内存模型

std::atomic 利用底层硬件的原子指令(如 x86 的 LOCK 前缀指令)或操作系统提供的原子操作接口,实现对变量的原子读写。C++11 还引入了内存序(memory order)概念,允许开发者控制可见性和有序性。

3.2 类型支持

std::atomic 支持如下类型:

  • 内置整数类型(如 int, bool

  • 指针类型(如 int*

  • 自定义类型(需满足 TriviallyCopyable)

四、std::atomic 的用法详解

4.1 基本用法

#include <atomic>
#include <thread>
#include <iostream>

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

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter++;  // 原子操作
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // 输出: 2000
    return 0;
}

4.2 主要操作

4.2.1 基本读写操作

std::atomic<int> value(10);

// 读取
int x = value.load();
int y = value;  // 等价于 load()

// 写入
value.store(20);
value = 30;     // 等价于 store()

4.2.2 交换操作

std::atomic<int> value(10);

// 原子交换
int old_value = value.exchange(20);  // old_value = 10, value = 20

// 比较并交换
int expected = 20;
bool success = value.compare_exchange_weak(expected, 30);
// 如果 value == expected,则设置为 30,返回 true
// 否则 expected 被设置为 value 的当前值,返回 false

4.2.3 算术操作(仅适用于整数类型)

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

counter++;           // 原子递增
counter--;           // 原子递减
counter += 5;        // 原子加法
counter -= 3;        // 原子减法

// 返回操作前的值
int old_val = counter.fetch_add(10);
int prev_val = counter.fetch_sub(5);

4.2.4 常用类型别名

std::atomic<bool>           // std::atomic_bool
std::atomic<int>            // std::atomic_int
std::atomic<long>           // std::atomic_long
std::atomic<size_t>         // std::atomic_size_t
std::atomic<void*>          // std::atomic_ptr

代码说明

  • load()store() 提供了原子读写接口。

  • ++--+= 等运算符已重载,都是原子操作。

4.3 复杂操作:CAS(Compare And Swap)

compare_exchange_strongcompare_exchange_weak 是 C++ 中 std::atomic 提供的两种原子性比较并交换(CAS, Compare-And-Swap)方法。它们的作用是:如果原子变量当前值等于 expected,则用新值替换,并返回 true;否则,把原子变量当前值写回 expected,并返回 false。

但二者有细微区别:

4.3.1 compare_exchange_strong

bool compare_exchange_strong(T& expected, T desired,
    std::memory_order success = std::memory_order_seq_cst,
    std::memory_order failure = std::memory_order_seq_cst);
  • 只在 expected 与原子变量当前值不相等时才会失败。

  • 如果原子变量当前值等于 expected,则将其替换为 desired。

  • 不会因为伪失败(spurious failure)而返回 false,即只有真正比较失败才返回 false。

  • 适合用于不希望重试的场景。

4.3.2 compare_exchange_weak

bool compare_exchange_weak(T& expected, T desired,
    std::memory_order success = std::memory_order_seq_cst,
    std::memory_order failure = std::memory_order_seq_cst);
  • 可能会因为伪失败而返回 false,即使 expected 与原子变量当前值相等。

  • 适合用于循环重试的场景(比如 lock-free 算法),通常配合 while 循环使用。

  • 伪失败的产生是为了优化性能,特别是在某些硬件上。

4.3.3 典型用法示例

#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> value(10);
    int expected = 10;

    // strong 只会在真正失败时返回 false
    if (value.compare_exchange_strong(expected, 20)) {
        std::cout << "Success, value = " << value << std::endl;
    } else {
        std::cout << "Fail, expected = " << expected << std::endl;
    }

    // weak 可能会伪失败,需要循环
    expected = 20;
    while (!value.compare_exchange_weak(expected, 30)) {
        // 可能需要多次尝试
        // 如果失败,expected 会被更新为当前 value 的值
    }
    std::cout << "After weak CAS, value = " << value << std::endl;

    return 0;
}

4.3.4 伪失败(Spurious Failure)说明

compare_exchange_weak 可能会偶尔失败,即使 expected 与原子变量当前值相等。这是硬件优化带来的副作用。有些平台的原子指令(比如某些 ARM、PowerPC 架构)在实现无锁原子操作时,可能会因为总线冲突、缓存一致性协议等原因导致操作失败。这样做可以提升性能,因为强制所有比较都必须成功会导致更复杂的硬件实现和更大的性能损失。

因此,compare_exchange_weak 一般用于循环重试:

do {
    expected = ...;
} while (!atomic_var.compare_exchange_weak(expected, desired));

compare_exchange_strong:只在真正比较失败时返回 false,适合不需要重试的场景。

compare_exchange_weak:可能伪失败,适合循环重试,性能更优。

4.4 内存序(memory order)

内存序(Memory Order)是C++多线程和原子操作中的一个重要概念,用于控制多线程程序中不同线程对共享变量访问的可见性和操作顺序。它决定了编译器和CPU在执行原子操作时,是否可以对读写操作进行重排序,以及这些操作对其它线程的可见性。

4.4.1 为什么需要内存序?

在多线程环境下,编译器和CPU为了优化性能,可能会对指令进行重排序。如果没有合适的内存序控制,不同线程看到的变量状态可能不一致,导致数据竞争和难以调试的bug。

4.4.2 C++中的常用内存序

C++标准库 <atomic> 提供了如下几种内存序(std::memory_order):

内存序类型

说明

memory_order_relaxed

最宽松的顺序,不保证任何顺序,仅保证原子操作本身的原子性。

memory_order_consume

消费语义,已废弃,类似 acquire,但只保证依赖性。

memory_order_acquire

获取语义,保证该操作之后的所有读写不会被重排到该操作之前。

memory_order_release

释放语义,保证该操作之前的所有读写不会被重排到该操作之后。

memory_order_acq_rel

获取+释放语义,结合了 acquire 和 release 的效果。

memory_order_seq_cst

顺序一致性,最严格的顺序保证,所有线程看到的原子操作顺序一致(默认值)。

4.4.3 内存序的举例说明

1. 顺序一致性(memory_order_seq_cst

最安全、最常用,保证所有原子操作有全局一致的顺序。

std::atomic<int> x{0};
x.store(1, std::memory_order_seq_cst);
int y = x.load(std::memory_order_seq_cst);

2. 释放-获取语义(memory_order_release/acquire

常用于生产者-消费者场景:

std::atomic<bool> ready{false};
int data = 0;

// 线程A(生产者)
data = 42;
ready.store(true, std::memory_order_release);

// 线程B(消费者)
while (!ready.load(std::memory_order_acquire));
int result = data; // 能保证看到最新的 data

3. 宽松顺序(memory_order_relaxed

只保证原子性,不保证操作顺序和可见性:

std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);
  • 内存序控制原子操作在多线程环境下的可见性和顺序。

  • 不同的内存序适用于不同的场景,选择合适的内存序有助于写出高效又安全的并发代码。

  • 默认使用 memory_order_seq_cst (顺序一致性)最安全,但性能可能较低。追求更高性能时,可以选择更宽松的内存序,但需要确保不会引入竞态条件。

五、适用场景

  • 计数器、标志位等简单共享变量。

  • 实现无锁数据结构(如无锁队列、无锁栈等)。

  • 替代部分场景下的互斥锁,提高并发性能。

六、与其他同步机制对比

同步方式

优点

缺点

适用场景

std::atomic

无锁高性能,避免死锁

仅适合简单场景,无等待队列

原子变量、无锁结构

std::mutex

适用复杂临界区,易用

存在锁竞争,可能死锁

复杂共享数据

volatile

仅保证不优化,不保证原子性

不能用于同步

特殊硬件寄存器

示例对比

使用互斥锁实现计数器:

#include <mutex>

int counter = 0;
std::mutex mtx;

void Increment() {
  std::lock_guard<std::mutex> lock(mtx);
  ++counter;
}

使用原子变量实现计数器:

#include <atomic>

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

void Increment() {
  ++counter;  // 原子自增,无需加锁
}

七、进阶扩展

7.1 原子智能指针

C++20 引入了 std::atomic<std::shared_ptr<T>>,支持原子操作智能指针,适合复杂对象的无锁共享。在 C++20 之前,std::shared_ptr 不是原子的,不能直接用 std::atomic 包装。C++20 后,std::atomic<std::shared_ptr<T>> 支持原子 load、store、exchange、compare_exchange_weak/strong 等操作。

注意:它保证的是指针本身的原子操作,不保证被指向对象的线程安全

#include <iostream>
#include <memory>
#include <atomic>
#include <thread>

struct Data {
    int value;
};

std::atomic<std::shared_ptr<Data>> global_ptr;

void reader() {
    auto ptr = global_ptr.load();
    if (ptr) {
        std::cout << "Read value: " << ptr->value << std::endl;
    }
}

void writer() {
    auto ptr = std::make_shared<Data>();
    ptr->value = 42;
    global_ptr.store(ptr);
}

int main() {
    global_ptr = std::make_shared<Data>();
    global_ptr.load()->value = 1;

    std::thread t1(reader);
    std::thread t2(writer);

    t1.join();
    t2.join();

    return 0;
}

支持的原子操作:

  • load():原子读取指针。

  • store():原子写入指针。

  • exchange():原子交换指针。

  • compare_exchange_weak/strong():原子比较并交换。

示例:compare_exchange_strong

std::atomic<std::shared_ptr<Data>> atomic_ptr;
auto old_ptr = atomic_ptr.load();
auto new_ptr = std::make_shared<Data>();
new_ptr->value = 100;

bool success = atomic_ptr.compare_exchange_strong(old_ptr, new_ptr);
if (success) {
    // 替换成功
} else {
    // old_ptr 已被更新为当前值
}

7.2 原子标志位(std::atomic_flag)

std::atomic_flag 是最轻量级的原子类型,常用于实现自旋锁。

#include <atomic>

std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;

void Lock() {
  while (lock_flag.test_and_set(std::memory_order_acquire));
}

void Unlock() {
  lock_flag.clear(std::memory_order_release);
}

八、常见误区

  1. 误用 volatilevolatile 不能替代原子操作,不保证线程安全。

  2. 滥用原子操作:复杂数据结构建议用锁保护,原子操作仅适合简单数据。

  3. 忽略内存序:默认顺序一致性足够,但极端性能场景需合理选择内存序。

九、总结

std::atomic 提供了高效、类型安全的原子操作,是现代 C++ 多线程编程的重要工具。合理运用原子操作,可以构建高性能的无锁并发程序。但在面对复杂数据结构和业务逻辑时,仍需谨慎评估其适用性,必要时配合互斥锁等传统同步机制。

0

评论区