C++ -- 原子变量

在C++中,原子变量主要通过标准库提供的std::atomic模板类来实现。

它利用硬件层面的原子指令(如CAS、LOCK前缀指令灯)确保对共享变量的操作是不可分割的,

无需使用传统的锁机制(synchronized或mutex),从而避免多线程环境下的数据竞争。

1、什么是原子操作

原子操作是指不可中断的操作序列。它要么完全执行成功,要么完全不执行,不存在中间状态。

传统问题:例如i++操作,看似一行代码,实际包含"读取值-》修改值-》写回值"三个步骤。

在多线程环境下,若两个线程同时执行,可能导致数据竞争,最终结果错误。

原子操作解决原子变量将这三个步骤合并为一个不可分割的硬件指令,确保同一时刻只有一个线程能成功修改值。

std::atomic 是 C++11 引入的标准库组件,定义在 <atomic> 头文件中。它的主要作用是提供‌原子操作‌,确保在多线程环境下对共享变量的读写是"不可分割"的,从而避免数据竞争(Data Race)和未定义行为。

1. 基本声明与初始化

std::atomic 是一个模板类,使用时需指定类型。它‌不可复制 ‌也‌不可移动‌。

cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>

// 1. 默认初始化(值不确定,不推荐直接使用)
std::atomic<int> a; 

// 2. 直接初始化(推荐)
std::atomic<int> b{0};      // C++11 列表初始化
std::atomic<bool> c(false); // 构造函数初始化

// 3. 使用便捷别名(等价于 std::atomic<T>)
std::atomic_int d{10};      // 等价于 std::atomic<int>
std::atomic_bool e{true};   // 等价于 std::atomic<bool>

2. 支持的类型

并非所有类型都能用于 std::atomic。类型必须满足 ‌TriviallyCopyable‌(可平凡复制)且通常要求是标准布局类型。

  • 整数类型 ‌:bool, char, int, long, long long 及其无符号版本等。
    • 特权 :支持额外的算术运算(如 fetch_add, fetch_sub)和位运算(fetch_and, fetch_or 等)。
  • 指针类型 ‌:T*
    • 特权 :支持指针算术运算(如 fetch_add 用于指针偏移)。
  • 浮点类型 ‌:float, double, long double (C++20 起完善支持)。
    • 特权 :支持 fetch_add, fetch_sub
  • 智能指针 ‌:std::shared_ptr, std::weak_ptr (C++20 起支持)。
  • 自定义类型 ‌:只要满足 std::is_trivially_copyable<T>::value 为 true,即可使用主模板 std::atomic<T>。但注意,对于较大的自定义结构体,原子操作可能通过内部锁实现,性能不如内置类型。

2、基本操作

2.1 加载与存储 (Load & Store)

load():原子读取值

store(T desired): 原子写入值

cpp 复制代码
std::atomic<int> val{0};

// 读取:原子地获取当前值
int current = val.load(); 
// 也可以指定内存序: val.load(std::memory_order_acquire);

// 写入:原子地设置新值
val.store(100);
// 也可以指定内存序: val.store(100, std::memory_order_release);
2.2 交换 (Exchange)

原子地将变量设置为新值,并返回旧值。

cpp 复制代码
int old_val = val.exchange(200); 
// val 现在变为 200,old_val 保存了交换前的值

2.3 读-改-写 (Read-Modify-Write, RMW)

这些操作将读取、修改和写回合并为一个原子步骤,常用于计数器或状态标志。

  • 增加/减少 ‌ (fetch_add, fetch_sub):
  • 位运算 ‌ (fetch_and, fetch_or, fetch_xor):仅适用于整数类型。
cpp 复制代码
int prev = val.fetch_add(1); // 原子加 1,返回加之前的值
val.fetch_sub(1);            // 原子减 1
cpp 复制代码
val.fetch_or(0x01); // 原子设置最低位
2.4 比较并交换 (Compare-And-Swap, CAS)

CAS 是无锁编程(Lock-Free Programming)的基石。它比较当前值与预期值,如果相等则更新为新值,否则不更新。

  • ‌**compare_exchange_strong** ‌:如果比较失败,一定会更新 expected 为当前实际值。适合大多数场景。
  • ‌**compare_exchange_weak**‌:允许"伪失败"(即使值相等也可能返回 false),通常在循环中使用,性能在某些架构上略高。

CAS操作

CAS是Compare-And-Swap(比较并交换)的缩写。是并发编程中实现无锁(Lock-Free)数据结构的核心原子操作指令。

CAS是一种原子操作,它包含三个操作数:

--- 内存位置的值(V):当前共享变量的实际值。

--- 预期原值(A,Expected):线程在操作前读取到的"旧值"。

--- 新值(B, Desired): 线程希望更新成的"新值"。

cpp 复制代码
std::atomic<int> val{10};
int expected = 10;
int desired = 20;

// 如果 val == expected (10),则将 val 设为 desired (20),返回 true
// 如果 val != expected,则将 expected 更新为 val 的当前值,返回 false
bool success = val.compare_exchange_strong(expected, desired);

if (success) {
    std::cout << "交换成功,val 现在是 20" << std::endl;
} else {
    std::cout << "交换失败,val 当前值是: " << expected << std::endl;
}

逻辑如下:

比较(Compare):检查内存位置V的当前值是否等于预期值A

交换(Swap):

如果相等(说明在此期间没有其他线程修改过该变量),则将V更新为新值B,并返回true(成功)

如果不相等(说明有其他线程抢先修改了变量),则不进行更新,并将内存中的最新实际值写回给预期值变量A,返回false(失败)

整个过程由硬件指令(如x86的CMPCHG或ARM的LDXR/STXR)保证原子性,不会被线程调度中断

参数行为关键点
  • ‌**expected 是引用传递** ‌:
    • 作为‌输入‌:传入你期望的旧值。
    • 作为‌输出 ‌:如果 CAS 失败,函数会将内存中的‌最新实际值 ‌写入 expected。这使得在下一次循环重试时,你可以直接使用更新后的 expected 进行比较,而无需重新加载内存值。
  • ‌**desired 是值传递**‌:你希望设置的新值。
cpp 复制代码
#include <atomic>
#include <iostream>

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

void atomic_increment() {
    int expected = counter.load(); // 1. 读取当前值
    int desired;
    
    // 2. 循环尝试 CAS
    do {
        desired = expected + 1; // 基于最新快照计算新值
        // 3. 尝试原子交换
        // 如果成功:counter 被更新为 desired,返回 true,循环结束
        // 如果失败:expected 被自动更新为 counter 的最新值,返回 false,继续循环
    } while (!counter.compare_exchange_weak(expected, desired));
}
cpp 复制代码
void atomic_increment(std::atomic<int>& val) {
    int old_val = val.load();
    while (!val.compare_exchange_weak(old_val, old_val + 1)) {
        // 如果失败,old_val 已经被更新为最新值,循环重试
    }
}
cpp 复制代码
std::atomic<bool> ready{false};
int data = 0;

// 线程 1: 生产者
void producer() {
    data = 42; // 非原子写入
    ready.store(true, std::memory_order_release); // 释放:确保 data=42 对消费者可见
}

// 线程 2: 消费者
void consumer() {
    while (!ready.load(std::memory_order_acquire)) { // 获取:等待 ready 为 true
        // 忙等待
    }
    std::cout << data << std::endl; // 安全读取,因为 acquire 保证了能看到 release 之前的写入
}

3. 常见误区与注意事项

  1. 不是万能锁 ‌:std::atomic 适合简单的变量操作(如计数器、标志位)。对于复杂的逻辑(如"检查-然后-执行"多步操作,或保护复杂数据结构如链表、树),仍需使用 std::mutex。强行用 atomic 实现复杂逻辑容易导致代码极其复杂且易错。
  2. 性能考量‌:虽然 atomic 比 mutex 轻量,但在高竞争下,CAS 失败导致的自旋重试会消耗 CPU。对于低竞争场景,atomic 优势明显;高竞争复杂场景,mutex 可能更优。
  3. 不要混合使用 ‌:如果一个变量被声明为 std::atomic,所有对该变量的访问都应通过 atomic 接口进行。不要直接用 = 赋值或读取(尽管编译器可能允许,但这破坏了内存序保证)。
  4. ‌**volatile 不等于 atomic** ‌:volatile 仅禁止编译器优化,不保证硬件层面的原子性和线程间可见性。多线程同步必须用 std::atomic 或互斥锁。
cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

class AtomicCounter {
private:
    // std::atomic_int 是 std::atomic<int> 的别名
    std::atomic<int> count_{0};

public:
    // 原子自增
    void increment() {
        // memory_order_relaxed 仅保证原子性,不保证与其他变量的顺序
        // 适用于纯计数场景,性能最高
        count_.fetch_add(1, std::memory_order_relaxed);
    }

    // 原子读取
    int get() const {
        return count_.load(std::memory_order_relaxed);
    }
    
    // 重置计数器
    void reset() {
        count_.store(0, std::memory_order_relaxed);
    }
};

// 测试代码
void worker(AtomicCounter& counter, int iterations) {
    for (int i = 0; i < iterations; ++i) {
        counter.increment();
    }
}

int main() {
    AtomicCounter counter;
    const int num_threads = 10;
    const int iterations_per_thread = 100000;

    std::vector<std::thread> threads;
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(worker, std::ref(counter), iterations_per_thread);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Expected: " << num_threads * iterations_per_thread << std::endl;
    std::cout << "Actual:   " << counter.get() << std::endl;

    return 0;
}
关键注意事项
  • 内存序选择 ‌:
    • std::memory_order_relaxed:最快,仅保证操作原子。适用于只关心最终总和,不依赖计数器值做同步决策的场景。
    • std::memory_order_seq_cst(默认):最强一致性,开销稍大。如果计数器用于控制程序流程(如"当计数达到 N 时触发事件"),建议使用默认值或 acquire/release
  • 避免隐式转换 ‌:std::atomic<int> 不能直接赋值给 int,必须使用 .load()
  • 前置 vs 后置++ ‌:++countercounter.fetch_add(1) 效果类似,但 fetch_add 语义更明确,且可以指定内存序。后置 counter++ 通常多一次 load 操作,性能略低。
cpp 复制代码
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>

class MutexCounter {
private:
    int count_ = 0;
    mutable std::mutex mtx_; // mutable 允许在 const 方法中加锁

public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx_);
        ++count_;
    }

    int get() const {
        std::lock_guard<std::mutex> lock(mtx_);
        return count_;
    }
};
优缺点
  • 优点‌:逻辑简单,容易扩展保护更多共享数据。
  • 缺点 ‌:高并发下性能瓶颈明显。线程争用锁时会发生内核态切换(休眠/唤醒),导致延迟不可控。在轻度竞争下,std::atomic 通常比 std::mutex 快 5-20 倍。

std::shared_mutex 可以允许多个线程同时读取,仅在写入时独占。

std::unique_lock<std::shared_mutex> lock(mtx_); // 独占锁

std::shared_lock<std::shared_mutex> lock(mtx_); // 共享锁,可并发读

相关推荐
cany10002 小时前
C++ -- 队列std::queue
开发语言·c++
周末也要写八哥2 小时前
C++中单线程方式之无脑上锁
java·开发语言·c++
cany10002 小时前
C++ -- 动态内存分配和释放(new/delete)
开发语言·c++
xcyxiner3 小时前
ubuntu下 cmake初始化脚本 以及 qt依赖
c++·qt
周末也要写八哥3 小时前
Visual C++6.0下载安装流程及PDF学习手册资源
c++·学习·pdf
熬夜敲代码的猫3 小时前
AVL树(C++详解版)
数据结构·c++·算法
思麟呀3 小时前
C++工业级日志项目(七)日志器核心
linux·开发语言·c++·windows
郝学胜_神的一滴3 小时前
Qt 高级开发 019:从零定制登录窗口按钮、Logo 样式与交互悬浮效果
c++·qt
lcj25113 小时前
vector的基本使用 + 手搓成员变量 size capacity begin end operator[] reserve扩容 拷贝构造 赋值析构
开发语言·c++·笔记·面试