C++ atomic 原子操作

在多线程共享数据时,如果只是一个统计次数的整数计数器,或者一个控制启停的布尔开关,每次修改都专门配一把互斥锁会显得有些繁琐。针对这类单个简单变量的同步场景,C++ 在 <atomic> 头文件中提供了 std::atomic 原子类型类模板。

所谓的 “原子操作”,指的就是不可分割的操作。它在底层要么还没开始,要么一步完整执行完毕,中途不会被其他线程插队打断,因此不需要加锁就能避免数据竞争。

C++ atomic 基本用法

在 C++ 中,std::atomic 是一个类模板,使用时我们在尖括号 <> 里写上要包装的基础数据类型即可,最常见的是 int、long、bool 等。

语法:

#include <atomic>

std::atomic<数据类型> 变量名 = 初始值;

说明:

定义好 std::atomic 对象后,我们既可以调用它提供的成员函数,也可以直接使用常见的自增或复合赋值运算符。假设 a 是一个 std::atomic 对象,日常开发中常用的原子操作如下表所示。

常用原子操作
操作写法 功能说明 适用类型
a.load() 安全读取并返回当前值 所有 atomic 类型
a.store(val) 安全将当前值修改为 val 所有 atomic 类型
a.exchange(val) 一步将值替换为 val,并返回替换前的旧值 所有 atomic 类型
a.fetch_add(n) / a.fetch_sub(n) 一步加上或减去 n,并返回修改前的旧值 整数与指针 atomic 类型
a++ / ++a / a += n 一步完成自增或复合赋值运算 整数与指针 atomic 类型

std::atomic 最经典的用途,就是在多线程环境下实现共享计数器。多个线程同时对同一个 std::atomic<int> 变量执行累加时,不需要额外搭配互斥锁,每一次加 1 都会完整生效。下面我们来看一个例子。

示例 1:无锁多线程计数器

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

std::atomic<int> counter = 0;

void addCount() {
    for (int i = 0; i < 100000; i++) {
        counter++;    // 原子自增 1
    }
}

int main() {
    std::thread t1(addCount);
    std::thread t2(addCount);

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

    std::cout << "最终计数:" << counter.load() << "\n";
    return 0;
}

运行结果如下。

最终计数:200000

分析:

在这个例子中,我们将全局计数器 counter 定义为 std::atomic<int> 类型,并通过 std::thread 启动了两个子线程。每个子线程各自执行 100000 次 counter++;,每次自增都是不可打断的原子操作,最后主线程调用 counter.load() 读出的结果稳定为 200000。

如果我们只是想把计数器加 1,下面 3 种方式在效果上是等价的。

// 方式 1
counter++;

// 方式 2
++counter;

// 方式 3
counter.fetch_add(1);

平时如果只是单纯累加而不关心旧值,直接写 counter++ 最直观。而当我们需要在加上一个数的同时拿到 “修改之前的旧值” 时,调用 fetch_add() 就非常方便。比如多个线程依次领取排号票时,写一句 int ticket = nextId.fetch_add(1);,就能一步拿到自己领到的号码。

需要特别注意的是,虽然 std::atomic<int> 支持直接写 counter++ 和 counter += 1,但在多线程中千万不要把同一个原子变量拆在等号两边计算。下面这种写法就是错误的。

counter = counter + 1;

这是因为 counter = counter + 1; 会先读取右边的值,加 1 后再写回左边,相当于拆成了两次独立的原子操作,两次操作中间依然会被其他线程插队覆盖。另外,std::atomic 对象不支持拷贝构造或相互赋值(比如不能用另一个原子变量去初始化 std::atomic<int> b = counter;),定义时直接传入普通初值即可。

使用 load()、store() 与 exchange()

除了整数计数器,另一个经常用到的是原子布尔类型 std::atomic<bool>,常用来充当多线程共享的开关或状态标志。

其中,store() 用于写入新状态,load() 用于读取当前状态。而 exchange() 会把变量一步替换成传入的新值,同时把替换之前的 “旧值” 返回出来。利用这个特性,我们可以很方便地实现 “多个线程同时争抢一次性任务,只有第 1 个抢到的线程执行” 的效果。

示例 2:原子布尔标志控制

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

std::atomic<bool> isInitialized = false;

void initSystem(int id) {
    bool oldVal = isInitialized.exchange(true);    // 设为 true 并返回旧值
    if (!oldVal) {
        std::cout << "线程 " << id << ":抢先完成初始化\n";
    } else {
        std::cout << "线程 " << id << ":已初始化,直接跳过\n";
    }
}

int main() {
    std::cout << "初始状态:" << isInitialized.load() << "\n";

    std::thread t1(initSystem, 1);
    std::thread t2(initSystem, 2);
    t1.join();
    t2.join();

    isInitialized.store(false);                    // 重置为 false
    std::cout << "重置后状态:" << isInitialized.load() << "\n";
    return 0;
}

运行结果如下。

初始状态:0
线程 1:抢先完成初始化
线程 2:已初始化,直接跳过
重置后状态:0

分析:

主线程开头调用 isInitialized.load() 读取当前的布尔状态,控制台默认将 false 打印为 0。随后 t1 和 t2 同时运行 initSystem(),由于 exchange(true) 不可分割,两个线程会一前一后拿到不同的旧值:

  • 第 1 个到达的线程:把标志从 false 改成 true,并拿到修改前的旧值 false。因此 !oldVal 为真,进入 if 分支完成初始化。
  • 第 2 个到达的线程:此时标志已经是 true 了,它拿到的旧值也是 true。因此 !oldVal 为假,直接进入 else 分支跳过,保证初始化代码只执行一次。

atomic 能完全替代 mutex 吗?

既然 std::atomic 不用加锁就能保证线程安全,那是不是把所有共享变量都改成 atomic,以后就再也不需要互斥锁了呢?

实际上,这样理解是不正确的。std::atomic 只能保证 “单个变量、单次操作” 的原子性。如果一项业务操作需要分多步完成,或者需要同时修改两个相互关联的变量,我们就必须使用 std::mutex 互斥锁把整段操作保护起来。

// 正确方式
std::mutex mtx;
int winWidth = 800;
int winHeight = 600;
void resizeSafe() {
    std::lock_guard<std::mutex> lock(mtx);
    winWidth = 1920;
    winHeight = 1080;
}

// 错误方式
std::atomic<int> width = 800;
std::atomic<int> height = 600;
void resizeBad() {
    width.store(1920);
    height.store(1080);
}

在上面的 resizeBad() 中,虽然 width.store(1920) 和 height.store(1080) 各自都是原子操作,但它们合在一起并不是原子的。其他线程有可能恰好在这两行代码执行中间读取窗口尺寸,拿到 “1920 × 600” 这个只改了一半的错误尺寸。

因此,小伙伴们只需要记住这么一条经验:如果只是对单个独立的整数或布尔标志做一步读写或加减,优先使用轻便的 std::atomic。如果一次修改涉及多个关联变量、操作 STL 容器或包含多步判断逻辑,仍然应该使用互斥锁配合 std::lock_guard 把整段代码锁起来。

提示 千万不要使用 while (!flag.load()) 写死循环去等待一个原子标志变为 true,这样会让 CPU 一直空转浪费性能。需要让线程睡眠等待某个条件成立时,我们应该使用条件变量来实现。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号