文章目录
- [C++ std::atomic 详解:原子操作与并发安全](#C++ std::atomic 详解:原子操作与并发安全)
- [一、std::atomic 核心定义](#一、std::atomic 核心定义)
-
- [1.1 基本声明(C++11起)](#1.1 基本声明(C++11起))
- [1.2 核心前提](#1.2 核心前提)
-
- [1.2.1 T类型条件详解(满足/不满足说明)](#1.2.1 T类型条件详解(满足/不满足说明))
- [1. 可平凡复制(`std::is_trivially_copyable<T>::value == true`)](#1. 可平凡复制(
std::is_trivially_copyable<T>::value == true)) - [2. 可复制构造、可移动构造(`is_copy_constructible`、`is_move_constructible` 均为true)](#2. 可复制构造、可移动构造(
is_copy_constructible、is_move_constructible均为true)) - [3. 可复制赋值、可移动赋值(`is_copy_assignable`、`is_move_assignable` 均为true)](#3. 可复制赋值、可移动赋值(
is_copy_assignable、is_move_assignable均为true)) - [4. 非const/volatile修饰(`std::is_same<T, typename std::remove_cv<T>::type>::value == true`)](#4. 非const/volatile修饰(
std::is_same<T, typename std::remove_cv<T>::type>::value == true))
- [二、std::atomic 的核心特性](#二、std::atomic 的核心特性)
-
- [2.1 原子性保障](#2.1 原子性保障)
- [2.2 内存序控制(memory order)](#2.2 内存序控制(memory order))
- [2.3 不可复制/不可移动](#2.3 不可复制/不可移动)
- [2.4 锁无关(lock-free)特性](#2.4 锁无关(lock-free)特性)
- [三、std::atomic 的特化版本](#三、std::atomic 的特化版本)
-
- [3.1 整数类型特化(最常用)](#3.1 整数类型特化(最常用))
- [3.2 指针类型特化(atomic<U*>)](#3.2 指针类型特化(atomic<U*>))
- [3.3 C++20起:shared_ptr/weak_ptr特化](#3.3 C++20起:shared_ptr/weak_ptr特化)
- [3.4 类型别名(简化声明)](#3.4 类型别名(简化声明))
- [四、std::atomic 核心成员函数](#四、std::atomic 核心成员函数)
-
- [4.1 构造函数](#4.1 构造函数)
- [4.2 赋值与读写操作](#4.2 赋值与读写操作)
- [4.3 交换操作(exchange)](#4.3 交换操作(exchange))
- [4.4 比较并交换(CAS,Compare-And-Swap)](#4.4 比较并交换(CAS,Compare-And-Swap))
- [4.5 C++20起:等待/通知机制](#4.5 C++20起:等待/通知机制)
- 五、实战示例:原子计数器
- 六、注意事项与常见误区
-
- [6.1 原子操作仅针对"单次操作"](#6.1 原子操作仅针对“单次操作”)
- [6.2 避免过度依赖默认内存序](#6.2 避免过度依赖默认内存序)
- [6.3 自定义类型的原子操作限制](#6.3 自定义类型的原子操作限制)
- [6.4 编译器与链接注意事项](#6.4 编译器与链接注意事项)
- [6.5 与atomic_flag的区别](#6.5 与atomic_flag的区别)
- 七、版本兼容性与特性扩展
- 总结
C++ std::atomic 详解:原子操作与并发安全
在C++并发编程中,数据竞争(data race)是最常见且隐蔽的bug之一------当多个线程同时读写同一个非原子变量时,会导致未定义行为(undefined behavior)。为解决这一问题,C++11标准引入了<atomic>头文件,其中的std::atomic模板类是实现原子操作、保障并发安全的核心工具。本文将结合C++标准文档,全面解析std::atomic的用法、特性与底层逻辑。
一、std::atomic 核心定义
std::atomic是一个模板类,用于定义原子类型 。原子类型的核心特性是:对其的读写操作具有原子性------即多个线程同时访问时,不会出现"读取到中间状态"的情况,且无需额外加锁(如mutex)就能避免数据竞争。
1.1 基本声明(C++11起)
C++
#include <atomic>
// 1. 通用模板(T需满足特定条件,见下文)
template< class T >
struct atomic;
// 2. 指针类型的部分特化
template< class U >
struct atomic<U*>;
// 3. C++20起,shared_ptr/weak_ptr的部分特化(需包含<memory>)
template< class U >
struct atomic<std::shared_ptr<U>>;
template< class U >
struct atomic<std::weak_ptr<U>>;
// 4. C++23起,兼容性宏(<stdatomic.h>中)
#define _Atomic(T) /* 等价于std::atomic<T>(若合法)*/
1.2 核心前提
使用通用模板std::atomic<T>时,类型T必须同时满足以下所有条件,否则程序非法:
-
可平凡复制(
std::is_trivially_copyable<T>::value == true) -
可复制构造、可移动构造(
is_copy_constructible、is_move_constructible均为true) -
可复制赋值、可移动赋值(
is_copy_assignable、is_move_assignable均为true) -
非const/volatile修饰(
std::is_same<T, typename std::remove_cv<T>::type>::value == true)
示例:自定义可平凡复制类型可实例化std::atomic:
C++
struct Counters { int a; int b; }; // 可平凡复制
std::atomic<Counters> cnt; // 合法:满足所有条件
1.2.1 T类型条件详解(满足/不满足说明)
核心结论:只有同时满足4个条件,T才能用于std::atomic<T>;只要有1个不满足,程序非法,以下分点拆解(结合常见类型举例,更易理解):
1. 可平凡复制(std::is_trivially_copyable<T>::value == true)
满足的类型(常见):
-
所有基础类型:int、char、float、double、指针类型等;
-
自定义平凡结构体/类:成员均为平凡类型、无自定义构造/析构/赋值函数、无虚函数,如
struct Counters { int a; int b; };; -
数组类型:如
int[5](元素需满足平凡复制)。
不满足的类型(常见):
-
含自定义构造/析构/赋值的类:如
struct A { A() {}; };(自定义默认构造); -
含虚函数的类:如
struct B { virtual void f() {}; };; -
标准库非平凡类型:
std::string、std::vector、普通std::shared_ptr(非atomic特化版)。
2. 可复制构造、可移动构造(is_copy_constructible、is_move_constructible 均为true)
满足的类型(常见):
-
基础类型、平凡结构体(默认生成拷贝/移动构造);
-
自定义类:显式/隐式定义合法的拷贝/移动构造,如
struct C { C(const C&) = default; };。
不满足的类型(常见):
-
禁用拷贝/移动构造的类:如
struct D { D(const D&) = delete; };; -
含不可拷贝成员的类:如
struct E { std::atomic<int> num; };(atomic本身不可拷贝)。
3. 可复制赋值、可移动赋值(is_copy_assignable、is_move_assignable 均为true)
满足的类型(常见):
-
基础类型、平凡结构体(默认生成拷贝/移动赋值);
-
自定义类:显式/隐式定义合法的拷贝/移动赋值,如
struct F { F& operator=(const F&) = default; };。
不满足的类型(常见):
-
禁用拷贝/移动赋值的类:如
struct G { G& operator=(const G&) = delete; };; -
含不可赋值成员的类:如
struct H { std::atomic<int> num; };(atomic本身不可赋值)。
4. 非const/volatile修饰(std::is_same<T, typename std::remove_cv<T>::type>::value == true)
**满足的类型:*无cv修饰的类型,如 int、char、Counters(自定义平凡结构体)、int。
**不满足的类型:**带const/volatile的类型,如 const int、volatile char、const Counters、volatile int*。
示例:std::atomic<const int> num; 非法(T为const int,含const修饰)。
C++
struct Counters { int a; int b; }; // 可平凡复制
std::atomic<Counters> cnt; // 合法:满足所有条件
二、std::atomic 的核心特性
2.1 原子性保障
对std::atomic对象的单次操作(如赋值、读取)是原子的,无需额外同步。例如:
C++
std::atomic<int> num(0);
// 线程1
num = 10; // 原子写操作
// 线程2
int val = num; // 原子读操作:val要么是0,要么是10,不会是中间值
而若使用普通int变量,上述操作可能出现数据竞争,导致val的值不确定。
2.2 内存序控制(memory order)
std::atomic的成员函数(如store、load、exchange)支持指定内存序,用于控制原子操作与其他非原子操作的内存可见性和执行顺序,平衡并发性能与安全性。
常用内存序(详细可参考C++内存模型):
-
std::memory_order_seq_cst(默认):最强序,保证所有线程看到的操作顺序一致,性能开销最大。 -
std::memory_order_relaxed:最弱序,仅保证操作本身原子,不保证与其他操作的顺序,性能最优(适合计数器等场景)。 -
std::memory_order_acquire/release:用于同步"读-写"操作,acquire(读)保证后续操作不重排到其之前,release(写)保证之前的操作不重排到其之后。
2.3 不可复制/不可移动
std::atomic类禁用了拷贝构造、赋值运算符以及移动构造、移动赋值,因此原子对象无法复制或移动(避免原子性被破坏):
C++
std::atomic<int> a(5);
std::atomic<int> b = a; // 错误:禁用拷贝赋值
std::atomic<int> c(a); // 错误:禁用拷贝构造
2.4 锁无关(lock-free)特性
std::atomic对象可能是"锁无关"的------即其原子操作不依赖于互斥锁,而是通过CPU的原子指令(如x86的LOCK前缀)实现,性能更高。可通过两个接口判断:
-
成员函数
is_lock_free():判断当前对象是否锁无关。 -
静态常量
is_always_lock_free(C++17起):判断该类型的所有对象是否始终锁无关。
注意:并非所有std::atomic特化都是锁无关(如自定义大型结构体的原子类型,可能依赖锁实现)。
三、std::atomic 的特化版本
除通用模板外,C++标准为特定类型提供了std::atomic的部分特化,这些特化拥有通用模板不具备的额外成员函数和特性。
3.1 整数类型特化(最常用)
当T为以下整数类型时,std::atomic<T>提供原子算术/位运算 (如fetch_add、fetch_and):
-
字符类型:
char、char8_t(C++20)、char16_t、char32_t、wchar_t; -
标准整数类型:
short、int、long、long long及其无符号版本; -
<cstdint>中的typedef类型(如int32_t、uint64_t)。
常用额外成员函数
C++
std::atomic<int> num(0);
// 1. 原子增减(返回操作前的值)
num.fetch_add(1); // 等价于 ++num(原子),返回操作前的num值
num.fetch_sub(2); // 等价于 --num(原子),返回操作前的num值
// 2. 复合赋值(原子)
num += 3;
num -= 1;
// 3. 位运算(原子)
num.fetch_and(0x0F); // 原子与运算
num.fetch_or(0xF0); // 原子或运算
num.fetch_xor(0xFF); // 原子异或运算
// 4. 自增自减(前缀/后缀)
++num;
num++;
--num;
num--;
3.2 指针类型特化(atomic<U*>)
对所有指针类型U*,std::atomic<U*>提供原子的指针算术操作(如偏移计算),额外成员函数与整数类型类似,但操作对象是指针地址:
C++
int arr[5] = {1,2,3,4,5};
std::atomic<int*> ptr(arr);
ptr.fetch_add(1); // 指针向后偏移1个int(指向arr[1])
ptr -= 1; // 指针向前偏移1个int(指向arr[0])
++ptr; // 指向arr[1]
注意:指针的原子操作仅保证"地址访问原子",不保证指针指向内容的原子访问。
3.3 C++20起:shared_ptr/weak_ptr特化
C++20新增std::atomic<std::shared_ptr<U>>和std::atomic<std::weak_ptr<U>>特化,用于实现原子的智能指针操作------解决了普通shared_ptr多线程读写的数据竞争问题:
C++
#include <atomic>
#include <memory>
std::atomic<std::shared_ptr<int>> atomic_sp;
// 线程1:原子赋值
atomic_sp.store(std::make_shared<int>(10));
// 线程2:原子读取
auto sp = atomic_sp.load();
if (sp) {
std::cout << *sp << std::endl; // 安全访问
}
3.4 类型别名(简化声明)
标准库为常用原子类型提供了typedef,避免重复书写std::atomic<T>,例如:
-
atomic_bool:等价于std::atomic<bool>; -
atomic_int、atomic_long:对应std::atomic<int>、std::atomic<long>; -
atomic_uint32_t、atomic_size_t:对应std::atomic<uint32_t>、std::atomic<size_t>。
四、std::atomic 核心成员函数
所有std::atomic特化都提供以下通用成员函数,部分特化会在此基础上增加额外函数(如整数/指针的算术操作)。
4.1 构造函数
C++
// 1. 默认构造(值未初始化,需后续store赋值)
std::atomic<int> num1;
// 2. 初始化构造(C++11起,不可用拷贝构造)
std::atomic<int> num2(10);
// 3. 禁止拷贝/移动构造
std::atomic<int> num3(num2); // 错误
std::atomic<int> num4 = num2; // 错误
4.2 赋值与读写操作
C++
std::atomic<int> num(0);
// 1. 原子写操作(store)
num.store(10); // 默认memory_order_seq_cst,可指定内存序
num.store(20, std::memory_order_relaxed);
// 2. 原子读操作(load)
int val = num.load(); // 默认memory_order_seq_cst
val = num.load(std::memory_order_relaxed);
// 3. 重载赋值运算符(等价于store)
num = 30; // 原子写
// 4. 重载类型转换(等价于load)
val = num; // 原子读
4.3 交换操作(exchange)
原子地替换原子对象的值,并返回操作前的旧值(兼具读和写的原子性):
C++
std::atomic<int> num(10);
int old_val = num.exchange(20); // num变为20,old_val = 10
4.4 比较并交换(CAS,Compare-And-Swap)
CAS是并发编程中核心的原子操作,用于实现无锁算法。其逻辑是:若原子对象当前值等于预期值(expected),则将其替换为新值(desired);否则,将原子对象的当前值写入expected。
提供两个版本:
-
compare_exchange_strong:强版本,即使出现"伪失败"(值相等但操作未成功),也会重试直到成功或确定值不相等。伪失败定义:指CAS操作中,原子对象的当前值与预期值(expected)实际相等,但由于CPU缓存、总线竞争等硬件层面的原因,操作未能成功执行,返回false的情况(并非值不相等导致的失败)。 -
compare_exchange_weak:弱版本,可能出现伪失败(需配合循环使用),但性能更高(适合循环场景)。
C++
std::atomic<int> num(10);
int expected = 10;
int desired = 20;
// 强版本:若num == 10,则替换为20,返回true;否则返回false,expected更新为num当前值
bool success = num.compare_exchange_strong(expected, desired);
// 弱版本:可能伪失败,需循环判断
do {
expected = 10; // 每次重试前重置预期值
} while (!num.compare_exchange_weak(expected, desired));
4.5 C++20起:等待/通知机制
C++20为std::atomic新增了等待(wait)和通知(notify)成员函数,用于线程间同步,替代传统的条件变量(更轻量):其更轻量的核心原因是wait/notify直接与原子对象绑定,无需依赖额外的互斥锁(mutex),减少了锁的创建、销毁和上下文切换开销;而传统条件变量的原理的是:依赖互斥锁(如std::mutex)实现线程间的同步与互斥,线程调用wait()时会先释放绑定的互斥锁,然后阻塞自身,等待其他线程调用notify_one()/notify_all()发送通知,收到通知后线程会重新竞争获取互斥锁,才能继续执行,整个过程涉及锁的释放、竞争、获取,开销更高。
cpp
std::atomic<int> flag(0);
// 线程1:等待flag变为1
flag.wait(0); // 若flag == 0,则阻塞线程,直到被notify唤醒并flag != 0
// 线程2:通知等待的线程
flag.store(1);
flag.notify_one(); // 唤醒至少一个等待该原子对象的线程
// flag.notify_all(); // 唤醒所有等待该原子对象的线程
五、实战示例:原子计数器
以下示例对比原子计数器与非原子计数器,展示std::atomic的并发安全特性:
C++
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
// 原子计数器(并发安全)
std::atomic_int atomic_cnt(0);
// 非原子计数器(存在数据竞争)
int normal_cnt(0);
// 线程函数:循环自增10000次
void increment() {
for (int i = 0; i < 10000; ++i) {
++atomic_cnt; // 原子自增
++normal_cnt; // 非原子自增(数据竞争)
// 优化:使用relaxed内存序(计数器无需强序)
// atomic_cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
// 创建10个线程并发执行
std::vector<std::jthread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
// 等待所有线程结束
threads.clear();
// 输出结果
std::cout << "原子计数器结果:" << atomic_cnt << std::endl; // 恒为100000
std::cout << "非原子计数器结果:" << normal_cnt << std::endl; // 小于等于100000(随机)
return 0;
}
运行结果说明:原子计数器始终输出100000(10线程×10000次),而非原子计数器因数据竞争,结果会随机小于等于100000。
六、注意事项与常见误区
6.1 原子操作仅针对"单次操作"
std::atomic仅保证单次成员函数调用 的原子性,复合操作(如num = num + 1)并非原子操作------其底层会拆分为"load读取旧值→计算→store写入新值"三步,中间可能被其他线程打断:
cpp
std::atomic<int> num(0);
// 错误:复合操作非原子
num = num + 1; // 等价于 num.store(num.load() + 1),中间可能被打断
// 正确:使用原子成员函数
num.fetch_add(1); // 单次原子操作
++num; // 等价于fetch_add(1),原子操作
6.2 避免过度依赖默认内存序
默认内存序memory_order_seq_cst虽安全,但性能开销较大。在无需强序的场景(如计数器),应使用memory_order_relaxed优化性能;在需要同步非原子操作的场景,再使用acquire/release或seq_cst。
6.3 自定义类型的原子操作限制
通用模板std::atomic<T>仅支持"读写"原子操作(store、load、exchange等),不支持算术/位运算------若需自定义类型的原子算术操作,需手动实现。
6.4 编译器与链接注意事项
在GCC、Clang编译器中,部分std::atomic功能(如自定义类型、64位原子类型在32位系统上)需要链接-latomic库,否则会出现链接错误。
6.5 与atomic_flag的区别
std::atomic_flag是C++11提供的最基础的原子布尔类型 ,仅支持test_and_set(置1并返回旧值)和clear(置0)两个操作,且始终是锁无关的 ;而std::atomic<bool>可能依赖锁实现(非锁无关),功能更丰富(支持所有通用原子操作)。
七、版本兼容性与特性扩展
-
C++11:引入
std::atomic通用模板、整数/指针特化、类型别名; -
C++20:新增
std::atomic<shared_ptr/weak_ptr>特化、等待/通知机制、atomic_signed_lock_free/atomic_unsigned_lock_free类型别名; -
C++23:新增
_Atomic兼容性宏、扩展浮点类型特化、fetch_max/fetch_min(C++26起支持); -
C++26:新增
constexpr原子操作(__cpp_lib_constexpr_atomic)。
总结
std::atomic是C++并发编程中实现"无锁并发安全"的核心工具,其本质是通过CPU原子指令或底层锁,保证对原子对象的单次操作具有原子性,避免数据竞争。使用时需注意:
-
根据类型选择合适的特化版本(整数/指针/智能指针);
-
区分"单次原子操作"与"复合操作",避免误区;
-
合理选择内存序,平衡性能与安全性;
-
注意编译器链接要求与版本兼容性。
相较于互斥锁(mutex),std::atomic在轻量级并发场景(如计数器、标志位)中具有更高的性能;而在复杂并发逻辑(如多操作原子性)中,仍需结合互斥锁或其他同步机制使用。
(注:文档部分内容可能由 AI 生成)