【C++】atmoic原子操作与并发安全全解析

文章目录

  • [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_constructibleis_move_constructible 均为true))
      • [3. 可复制赋值、可移动赋值(`is_copy_assignable`、`is_move_assignable` 均为true)](#3. 可复制赋值、可移动赋值(is_copy_assignableis_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_constructibleis_move_constructible均为true)

  • 可复制赋值、可移动赋值(is_copy_assignableis_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::stringstd::vector、普通std::shared_ptr(非atomic特化版)。

2. 可复制构造、可移动构造(is_copy_constructibleis_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_assignableis_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 intvolatile charconst Countersvolatile 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的成员函数(如storeloadexchange)支持指定内存序,用于控制原子操作与其他非原子操作的内存可见性和执行顺序,平衡并发性能与安全性。

常用内存序(详细可参考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_addfetch_and):

  • 字符类型:charchar8_t(C++20)、char16_tchar32_twchar_t

  • 标准整数类型:shortintlonglong long及其无符号版本;

  • <cstdint>中的typedef类型(如int32_tuint64_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_intatomic_long:对应std::atomic<int>std::atomic<long>

  • atomic_uint32_tatomic_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/releaseseq_cst

6.3 自定义类型的原子操作限制

通用模板std::atomic&lt;T&gt;仅支持"读写"原子操作(storeloadexchange等),不支持算术/位运算------若需自定义类型的原子算术操作,需手动实现。

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原子指令或底层锁,保证对原子对象的单次操作具有原子性,避免数据竞争。使用时需注意:

  1. 根据类型选择合适的特化版本(整数/指针/智能指针);

  2. 区分"单次原子操作"与"复合操作",避免误区;

  3. 合理选择内存序,平衡性能与安全性;

  4. 注意编译器链接要求与版本兼容性。

相较于互斥锁(mutex),std::atomic在轻量级并发场景(如计数器、标志位)中具有更高的性能;而在复杂并发逻辑(如多操作原子性)中,仍需结合互斥锁或其他同步机制使用。

(注:文档部分内容可能由 AI 生成)

相关推荐
Coder个人博客2 小时前
Linux6.19-ARM64 mm mmap子模块深入分析
大数据·linux·安全·车载系统·系统架构·系统安全·鸿蒙系统
zz34572981132 小时前
C语言基础概念7
c语言·开发语言
会开花的二叉树2 小时前
Reactor网络库的连接管理核心:Connection类
开发语言·网络·php
凯子坚持 c2 小时前
C++基于微服务脚手架的视频点播系统---客户端(1)
开发语言·c++·微服务
袖清暮雨2 小时前
Python爬虫(Scrapy框架)
开发语言·爬虫·python·scrapy
CSDN_RTKLIB2 小时前
SharedPtr测试步骤说明
c++
呱呱巨基2 小时前
Linux 第一个系统程序 进度条
linux·c++·笔记·学习
2401_838472512 小时前
C++中的装饰器模式实战
开发语言·c++·算法
沐知全栈开发2 小时前
PHP 数组
开发语言