在多线程编程中,多个线程同时读写同一个共享变量时,很容易出现数据竞争问题,导致程序出现不可预期的运行结果。C++11标准引入的atomic模板类提供了原子操作支持,可以在无锁的情况下保证共享变量的线程安全,是多线程开发中常用的工具。

atomic基础概念
原子操作指的是不可被中断的一个或一组操作,执行过程中不会被其他线程的操作干扰。C++的atomic模板可以包装任意可平凡复制的类型,被包装后的变量所有读写操作都是原子的,不需要额外的同步措施。
和互斥锁相比,原子操作的性能开销更小,适合轻量级的共享变量同步场景,但无法处理复杂的临界区逻辑。
atomic基本用法
定义原子变量
使用std::atomic定义原子变量时,需要包含<atomic>头文件,基本定义方式如下:
#include <atomic> #include <iostream> // 定义原子int类型变量,初始值为0 std::atomic<int> counter(0); // 定义原子bool类型变量 std::atomic<bool> flag(false);
常用原子操作
atomic提供了多种原子操作接口,常用的包括读写、自增自减、交换等:
- store:原子写操作,将值写入原子变量
- load:原子读操作,读取原子变量的当前值
- fetch_add:原子加操作,返回加之前的值
- fetch_sub:原子减操作,返回减之前的值
- exchange:原子交换操作,将新值写入原子变量并返回旧值
- compare_exchange_strong:原子比较交换操作,若当前值等于期望值则替换为新值
实战示例:多线程计数器
下面通过一个多线程计数器的示例,展示原子操作的实际使用效果。如果不使用原子操作,多个线程同时修改同一个计数器会出现计数丢失的问题。
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
// 定义原子计数器,初始值为0
std::atomic<int> global_counter(0);
// 每个线程执行的函数,对计数器加1000次
void increment_task() {
for (int i = 0; i < 1000; i++) {
// 原子自增操作,等价于global_counter++,但保证线程安全
global_counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> threads;
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment_task);
}
// 等待所有线程执行完成
for (auto& t : threads) {
t.join();
}
// 输出最终计数器的值,预期为10*1000=10000
std::cout << "最终计数器值: " << global_counter.load() << std::endl;
return 0;
}
上述代码中,10个线程每个对计数器加1000次,使用原子操作后最终输出结果一定是10000,不会出现计数丢失的情况。如果去掉原子操作,改用普通int变量,最终结果通常会小于10000。
内存顺序说明
原子操作的接口通常可以指定内存顺序参数,用于控制操作的同步和排序效果,常用的内存顺序有:
| 内存顺序 | 说明 |
|---|---|
| std::memory_order_relaxed | 宽松内存顺序,只保证操作原子性,不保证同步和排序,性能最高 |
| std::memory_order_acquire | 获取操作,当前线程后续的所有读写操作都不能重排到该操作之前 |
| std::memory_order_release | 释放操作,当前线程之前的所有读写操作都不能重排到该操作之后 |
| std::memory_order_seq_cst | 顺序一致性内存顺序,默认选项,保证所有线程看到的操作顺序一致,开销最大 |
对于简单的计数器场景,使用std::memory_order_relaxed就足够,不需要更强的内存顺序保证,这样可以获得更好的性能。
使用注意事项
- 只有
atomic包装的变量的操作才是原子的,对原子变量的普通读写操作如果没有通过原子接口调用,仍然可能不是原子的 - 原子操作只保证单个变量的操作原子性,无法处理多个变量组合的临界区逻辑,这种场景还是需要互斥锁
- 不是所有类型都可以作为
atomic的模板参数,模板参数类型需要满足可平凡复制的要求 - 原子变量的初始化不能使用普通赋值,需要在构造时初始化,或者使用
store接口赋值
原子操作是C++多线程编程中高效的同步手段,合理使用可以大幅提升程序性能,开发者需要根据实际场景选择合适的原子操作和内存顺序,避免过度使用或者错误使用。