一、原子性与数据竞争

原子性(atomicity):一个操作要么全部完成、要么全部不发生 —— 对并发线程来说,其他线程不会看到“中间状态”。
数据竞争(data race):两个或更多线程并发访问同一内存位置,并且至少有一个写操作,且这些访问没有被某种同步机制(原子操作或互斥)序列化 —— 根据 C++ 标准,这会导致未定义行为。

简言之:想避免数据竞争,就需要同步——原子操作或(mutex/condition_variable 等)。

1.1、原子操作与锁的比较

特性原子操作 (std::atomic)锁 (std::mutex, std::shared_mutex)
同步粒度单个变量级别任意代码块或多变量
实现机制硬件原子指令(如 x86 的 LOCK CMPXCHG内核/用户态互斥机制(可能导致线程切换)
性能特征轻量、高速(无锁/低延迟)较重、涉及线程调度(但可避免忙等)
阻塞特性非阻塞(busy-wait 或循环重试)阻塞等待(由 OS 调度唤醒)
适用场景计数器、标志位、无锁队列、引用计数数据结构保护、复杂状态同步、资源管理
可见性保证内存序(memory_order)控制自动保证可见性和顺序
可组合性差(多变量时复杂)强(可锁住多个对象或范围)
调试与维护难度高,容易出 subtle bug低,更易理解和维护

在实际工程中:

  • 原子操作 适合计数器、引用计数、自旋锁等;
  • 互斥锁(mutex) 适合复杂逻辑、跨多步操作的同步。

二、为什么需要原子操作?

考虑以下简单的计数器示例:

#include <iostream>
#include <thread>
#include <vector>

int counter = 0;

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

int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

理论上,最终结果应该是 1,000,000,但由于 ++counter 不是原子操作,实际运行结果往往小于这个值。
如果加锁的话只是对一个整数计数器 进行读写,每次操作只是一条简单的赋值、加减,如果还要为此加锁、解锁,显然显得过于“笨重”,既影响性能,又让代码更复杂。为了解决这种 “简单场景却要用锁”的尴尬,在C11中引入了std::atomic,使得代码更加高效。

三、C++原子类型

C++11 引入了 <atomic> 头文件,提供了跨平台、无锁的原子操作接口:

3.1、基本原子类型

#include <atomic>

struct CTest
{
    int a;
    int b;
};

//基本数据类型
std::atomic<char> ac('C');
std::atomic<int> ai(0);
std::atomic<bool> ab(0);
std::atomic<double> ad(6.66);

//指针类型
std::atomic<char*> ptr1(nullptr);
std::atomic<void*> ptr2(nullptr);

//自定义类型
std::atomic<CTest> test;

3.2、原子操作示例

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

std::atomic<int> atomic_counter{0};

void atomic_increment() {
    for (int i = 0; i < 100000; ++i) {
        atomic_counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(atomic_increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final atomic counter value: " << atomic_counter << std::endl;
    return 0;
}

这次,无论运行多少次,结果都是正确的 1,000,000。

四、常用的原子操作

4.1、加载和存储

std::atomic<int> value{0};

// 存储值
// atomic 支持通过 store 函数或直接赋值运算符来为原子变量设置值
value.store(42, std::memory_order_release);
value = 42;

// 读取值
int loaded = value.load(std::memory_order_acquire);

// 读取旧值,设置新值,返回旧值
int old_value = value.exchange(20);

4.2、读-修改-写操作

std::atomic<int> x{0};

// 原子加法
x.fetch_add(5);  	// 等同于 x += 5

// 原子减法
x.fetch_sub(3);  	// 等同于 x -= 3

// 原子与操作
x.fetch_and(0x0F);	// 等同于 x &= 0x0F

// 原子或操作
x.fetch_or(0xF0);	// 等同于 x |= 0xF0

// 原子异或操作
x.fetch_xor(0xFF);	// 等同于 x ^= 0xFF

4.3、比较交换(Compare-and-Swap)

CAS(Compare-And-Swap 或 Compare-And-Exchange)是多线程编程里最核心的原子操作,用来实现无锁同步。CAS 的操作逻辑是:

原子执行
if(原子变量当前值 == 预期值expected)
{
	原子变量当前值 = 新值desired;
	return true;
}
else
{
	原子变量当前值 = 预期值expected;
	return false;
}

示例:

std::atomic<int> data{0};

int expected = 0;
int desired = 42;

// 如果 data == expected,则 data = desired
bool success = data.compare_exchange_strong(expected, desired);
bool success = data.compare_exchange_weak(expected, desired);
  • strong:只要原子变量值和 expected 相等,就一定返回 true,更新成功。
  • weak:即使原子变量值和 expected 相等,也可能因为硬件或优化原因返回 false(虚假失败)。

两个版本的使用场景与原因

  • weak 版本:在条件满足时也可能返回 false,这种情况称为 虚假失败(spurious failure)。出现虚假失败的原因通常是底层硬件指令的限制。由于实现更简单,weak 在某些平台上往往比 strong 更高效,常用于循环重试的场景。
  • strong 版本:只要条件满足,就一定返回 true,不会出现虚假失败。不过实现上可能更复杂,在部分平台上性能略低于 weak

注意:在一些平台上,两者的实现完全相同,不存在性能差异。

五、内存顺序(Memory Order)

内存顺序指定了原子操作周围的内存访问如何排序,是理解原子操作的关键:
需注意的问题:

  1. 指令重排:编译器或 CPU 为了优化性能,可能调整代码执行顺序。在单线程里没问题,但在多线程中,别的线程可能看到的执行顺序和代码写的顺序不一致,从而出错。
  2. 修改顺序一致性:即使所有修改操作都是原子的,不同线程看到的修改先后次序也可能不一样,就像“历史版本”不统一。如果程序依赖于这个顺序,就可能出错。

5.1、六种内存顺序

#include <atomic>

std::atomic<int> x{0};
std::atomic<int> y{0};
int data = 0;

void memory_order_demo() {
    // 最宽松的顺序,只保证原子性
    x.store(1, std::memory_order_relaxed);
    
    // 释放操作:保证之前的读写不会重排到之后
    data = 42;
    x.store(2, std::memory_order_release);
    
    // 获取操作:保证之后的读写不会重排到之前
    int tmp = y.load(std::memory_order_acquire);
    
    // 顺序一致性:最严格的顺序,所有线程看到的操作顺序一致
    x.store(3, std::memory_order_seq_cst);
}

5.2、内存顺序使用场景

#include <atomic>
#include <thread>

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

void producer() {
    data = 42;  // 准备数据
    ready.store(true, std::memory_order_release);  // 发布数据
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {  // 等待数据就绪
        // 忙等待或 yield
    }
    std::cout << "Data: " << data << std::endl;  // 安全读取数据
}

六、原子标志和自旋锁

6.1、使用原子标志实现简单的自旋锁

#include <atomic>

class SpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 自旋等待
        }
    }
    
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

SpinLock spinlock;
int shared_data = 0;

void critical_section() {
    spinlock.lock();
    ++shared_data;  // 临界区
    spinlock.unlock();
}

七、原子指针

#include <atomic>

struct Node {
    int data;
    Node* next;
};

std::atomic<Node*> head{nullptr};

void push_front(int value) {
    Node* new_node = new Node{value, nullptr};
    new_node->next = head.load();
    
    while (!head.compare_exchange_weak(new_node->next, new_node)) {
        // CAS 失败,重试
    }
}

八、使用限制

8.1、为什么会有限制?

原子性(atomicity)在底层是由 CPU 指令直接支持的。

CPU 要实现“原子操作”,必须确保:

对目标内存的访问(读或写)不会被中断,也不会与其他线程/核心的访问交叉。

这意味着:

  • 操作必须在 单条 CPU 指令 内完成;
  • 操作的数据必须能 在总线事务中一次性完成读写

👉 所以:
原子操作的字节数必须在 CPU 硬件支持的“原子访问粒度”范围内。


8.2、不同 CPU 架构对原子访问的支持

架构原子访问支持的典型字节数说明
x86 / x641、2、4、8 字节LOCK 前缀保证总线锁
ARMv8 (AArch64)1、2、4、8、16 字节(部分情况)依赖 LDXR/STXR 指令对齐要求
RISC-V通常 4、8 字节AMO 指令对齐访问
PowerPC / SPARC通常 4、8 字节指令对齐访问

注意:

  • 未对齐的访问(unaligned access) 即使是支持的字节数,也可能无法原子完成。
  • 超过支持字节数(例如 16 字节的结构体)将无法使用无锁原子操作,C++ 库会自动使用锁模拟。

8.3、C++ 层面对原子操作字节数的规定

C++ 标准([ISO/IEC 14882])中并没有硬编码规定“多少字节必须原子”。
但它要求实现必须提供 std::atomic<T>,前提是该类型 可以在该平台上原子访问

1. 关键判断函数

std::atomic<T> a;
bool lockfree = a.is_lock_free();
  • 返回 true → 平台可用无锁原子指令
  • 返回 false → 实现内部会使用锁(例如 std::mutex)来模拟原子性

2. C++ 标准保证的最小支持

所有 指针类型整型类型(1、2、4字节) 都必须可作为原子类型。

也就是说:

  • std::atomic<bool>
  • std::atomic<char>
  • std::atomic<int>
  • std::atomic<long>
  • std::atomic<void*>

这些在任何实现上都是可用的。


8.4、实际编译器/平台支持差异(以 GCC/Clang + x86 为例)

类型大小是否 lock-free
std::atomic<bool>1 字节
std::atomic<char>1 字节
std::atomic<short>2 字节
std::atomic<int>4 字节
std::atomic<long>8 字节(x64)
std::atomic<long long>8 字节
std::atomic<void*>8 字节
std::atomic<__int128>16 字节⚠️ 仅部分平台支持
std::atomic<struct {...}>任意❌ 通常需锁实现

如果类型超过 8 字节,C++ 可能仍允许定义 std::atomic<T>,但其实现会自动退化为“带锁版本”。


8.5、对齐(Alignment)要求

1. 对齐要求的原因

原子操作必须在单个 CPU 总线事务中完成。
若变量的地址跨越缓存行或总线宽度边界,则可能无法原子化。

因此:

所有原子类型都必须是自然对齐(naturally aligned)的。

2. 举例说明

#include <atomic>
#include <iostream>
#include <cstddef>

struct alignas(8) MyData {
    int a;
    int b;
};

std::atomic<MyData> data; // 要求 8 字节对齐

如果编译器检测到 MyData 未满足对齐要求,可能报错或退化为锁实现。


8.6、超出硬件限制的数据怎么办?

当你的数据结构超过硬件原子大小(例如一个 64 字节的结构体),
std::atomic<T> 仍然可以声明,但实现将退化为“带锁”的形式。

例如:

struct BigData {
    int a[16]; // 64 字节
};

std::atomic<BigData> big; // 合法,但非 lock-free

在这种情况下:

  • big.is_lock_free()false
  • 编译器会在内部使用一个全局锁(std::mutex)来保证线程安全。

8.7、如何在工程中判断与使用

1. 检查是否 lock-free

std::atomic<int64_t> x;
if (x.is_lock_free())
    std::cout << "无锁原子操作\n";
else
    std::cout << "退化为锁实现\n";

2. 使用对齐控制

struct alignas(8) Counter {
    long value;
};
std::atomic<Counter> counter;

确保 alignas() 与 CPU 总线宽度(通常是 8)一致。

3. 控制结构体大小

如需保证 lock-free:

  • 仅包含基本类型(不超过 8 字节)
  • 避免嵌套数组或字符串
  • 明确对齐

九、最佳实践

  1. 选择合适的原子类型:对于简单数据类型,使用对应的原子类型

  2. 使用恰当的内存顺序:在保证正确性的前提下使用最宽松的内存顺序

  3. 避免虚假共享

    struct alignas(64) PaddedAtomic {  // 缓存行对齐
        std::atomic<int> value;
    };
    
  4. 谨慎使用 CAS 循环:避免在高竞争场景下的活锁

总结

C++原子操作为编写高效、正确的并发程序提供了强大的工具。理解原子性、内存顺序以及各种原子操作的使用场景,是掌握现代C++并发编程的关键。通过合理使用原子操作,我们可以在保证线程安全的同时,获得比传统锁机制更好的性能。

记住,原子操作虽然强大,但并不是所有并发问题的银弹。在设计并发系统时,应该根据具体需求选择最合适的同步机制。

Logo

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

更多推荐