在C++多线程编程中,std::atomic(原子操作)是实现无锁编程(Lock-Free)和轻量级同步 的核心机制。它定义在 <atomic> 头文件中。
1. 为什么要用 Atomic?
在多线程环境下,简单的 i++ 操作其实包含三个步骤:
- Read : 从内存读取
i的值到寄存器。 - Modify: 在寄存器中将值加 1。
- Write: 将新值写回内存。
如果没有加锁或使用原子变量,两个线程同时执行 i++,可能会导致所谓的数据竞争(Data Race),也就是丢失更新。
Atomic 的作用: 保证上述 Read-Modify-Write 过程合并为一个不可打断的硬件指令(如 x86 的 lock add),从而保证线程安全。
2. 基础用法
最常用的场景是计数器或标志位。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
// 定义一个原子整型,初始化为0
std::atomic<int> counter(0);
void work() {
for (int i = 0; i < 10000; ++i) {
counter++; // 原子自增,等价于 counter.fetch_add(1)
}
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
// 结果一定是 20000,如果是普通 int 则结果不确定
std::cout << "Result: " << counter << std::endl;
return 0;
}
注意: 虽然 counter++ 是原子的,但 counter = counter + 1不是原子的!因为它把读取和写入分成了两个独立的语句。
3. 核心成员函数 (API)
std::atomic<T> 提供了比简单的 ++ 更精细的控制:
|-------------------------|------------------------|-------------------|
| 函数名 | 描述 | 对应操作 |
| load() | 读取原子变量的值 | Read |
| store(val) | 修改原子变量的值 | Write |
| exchange(val) | 交换值:写入新值,并返回旧值 | Read-Modify-Write |
| fetch_add(val) | 加法:加上 val,并返回加之前的值 | Read-Modify-Write |
| fetch_sub(val) | 减法:减去 val,并返回减之前的值 | Read-Modify-Write |
| compare_exchange_* | CAS 操作(重点) | Compare-And-Swap |
4. CAS 操作 (Compare And Swap)
这是无锁编程的基石。它的逻辑是:"我认为当前内存里的值应该是 A(预期值),如果是,就把他改成 B(新值);如果不是,告诉我当前内存里到底是多少。"
C++ 提供了两个版本:
compare_exchange_strongcompare_exchange_weak(允许伪失败,效率更高,常用于循环中)
CAS 经典代码模板(实现一个无锁的原子更新):
void atomic_multiply(std::atomic<int>& current_val, int multiplier) {
int expected = current_val.load(); // 1. 读取当前值作为"预期值"
int desired;
do {
desired = expected * multiplier; // 2. 计算想要写入的新值
// 3. 尝试更新:
// 如果 current_val 依然等于 expected,则 current_val = desired,返回 true。
// 如果 current_val 不等于 expected(被其他线程改了),
// 则 expected = current_val (更新预期值为当前最新值),返回 false。
} while (!current_val.compare_exchange_weak(expected, desired));
}
这个循环被称为 CAS Loop 。它体现了乐观锁的思想:假设没人跟我抢,如果真有人抢了,我就重试。
5. 内存顺序 (Memory Order) - 进阶必读
std::atomic 的操作函数通常有一个可选参数 std::memory_order。这是 C++ 原子操作最难理解但也最强大的部分。它控制编译器和 CPU 如何对指令进行重排序。
默认情况下,所有原子操作都使用 std::memory_order_seq_cst(顺序一致性),这是最安全但性能消耗最大的模式。
常见的内存序:
- memory_order_relaxed**(松散序)**
-
- 只保证原子性,不保证顺序。
- 用途: 纯粹的计数器(如统计网页访问量),不涉及线程间的同步逻辑。
- memory_order_acquire**(获取)** & memory_order_release**(释放)**
-
- 成对使用,建立同步屏障。
- Release: 之前的写操作不允许重排到 Release 之后(保证写完了才能发信号)。
- Acquire: 之后的读操作不允许重排到 Acquire 之前(保证收到信号后才开始读)。
- 用途: 互斥锁的底层实现、生产者-消费者模型。
- memory_order_seq_cst**(顺序一致性)**
-
- 默认值。 保证所有线程看到的指令执行顺序是一致的。
- 用途: 对安全性要求高、性能不那么敏感的场景。
6. std::atomic 与 std::mutex 的对比
|----------|-----------------------------|---------------------|
| 特性 | std::atomic | std::mutex |
| 机制 | 硬件指令支持 (CPU Level) | 操作系统内核调度 (OS Level) |
| 阻塞 | 非阻塞 (Lock-Free),线程不会挂起 | 阻塞,线程会挂起等待唤醒 |
| 开销 | 极小,主要在 CPU 流水线和缓存 | 较大,涉及上下文切换 |
| 适用场景 | 简单数据类型 (int, ptr, bool) 的同步 | 复杂逻辑、大块代码段的同步 |
| 复杂性 | 需要理解内存序,容易写出 Bug | 逻辑简单清晰,不易出错 |
7.automic 实现无锁栈
// 实现一个无锁的线程安全栈
#include <iostream>
#include <atomic>
#include <vector>
#include <thread>
#include<unistd.h>
#include<chrono>
template <typename T>
struct Node
{
T _data;
Node *_next;
Node(const T &data) : _data(data), _next(nullptr)
{
}
};
template <typename T>
class LockFreeStack
{
using node = Node<T>;
using atomic_head = std::atomic<node *>;
private:
atomic_head _head;
public:
LockFreeStack() : _head(nullptr)
{
}
// 入栈操作,头插
void push(const T &val)
{
node *old_head = _head.load();
node *new_node = new node(val);
new_node->_next = old_head;
while (!_head.compare_exchange_weak(old_head, new_node))
{
new_node->_next = old_head;
}
}
// 出栈操作,去除头部节点
bool pop(T &result)
{
node *old_head = _head.load();
// 如果指定的头结点和现有的不同则将现有的给old_head,再进行一次
while (old_head && !_head.compare_exchange_weak(old_head, old_head->_next))
{
}
// 如果旧指针头部是空节点,证明栈为空
if (old_head == nullptr)
{
return false;
}
result = old_head->_data;
//std::cout<<result<<std::endl;
delete old_head;
return true;
}
};
int main()
{
LockFreeStack<int> st;
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++)
{
if (i < 5)
{
threads.push_back(std::thread([&]()
{ st.push(i + 10); }));
}
else
{
int count = 0;
// 这里可能会报错
threads.push_back(std::thread([&]()
{ st.pop(count); }));
sleep(1);//这里需要后sleep,不然可能主线程先执行,子线程还没执行
std::cout<<count<<std::endl;
}
}
for(int i=0;i<threads.size();i++)
{
threads[i].join();
}
return 0;
}