1. 简介
std::atomic
是 C++ 提供的一种用于多线程编程的同步原语,保证对数据类型的操作是原子的,即不会被中断或与其他线程产生冲突。它适用于需要在线程之间共享数据而不需要加锁的场景,提供比传统的锁(如 std::mutex
)更好的性能。
2. 常用场景
std::atomic
通常用于以下情况:
- 共享资源的安全访问:在多线程程序中,多个线程需要访问和修改同一变量。
- 避免锁机制的开销:锁会带来性能上的开销,而
std::atomic
提供了更轻量的原子操作。 - 简单的同步:当仅需要单个变量的同步时,使用
std::atomic
是非常高效的。
3. 常用原子操作
std::atomic
支持多种原子操作,包括:
- 加载 (
load
) - 存储 (
store
) - 交换 (
exchange
) - 比较并交换 (
compare_exchange_strong
,compare_exchange_weak
) - 增量和减量 (
fetch_add
,fetch_sub
)
4. std::atomic
支持的类型
- 内置基础类型:如
int
,bool
,char
,float
,double
等。 - 自定义类型:可以将自定义结构体/类声明为
std::atomic<T>
,但必须保证其满足 "trivially copyable"(平凡可拷贝)要求,即没有自定义构造、析构、拷贝构造和赋值运算符等。
5. 基本用法示例
5.1 基础操作
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter; // 原子加操作
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl; // 输出 2000
return 0;
}
解释:
std::atomic<int> counter(0)
:声明了一个初始值为 0 的原子整型变量counter
。++counter
:原子自增操作,确保多个线程同时访问时不会发生竞态条件。
5.2 compare_exchange 操作
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> value(10);
void compareAndSwap(int expected, int desired) {
if (value.compare_exchange_strong(expected, desired)) {
std::cout << "Swap successful! New value: " << value << std::endl;
} else {
std::cout << "Swap failed. Expected: " << expected << ", actual: " << value << std::endl;
}
}
int main() {
std::thread t1(compareAndSwap, 10, 20);
std::thread t2(compareAndSwap, 10, 30);
t1.join();
t2.join();
return 0;
}
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> value(10);
void compareAndSwap(int expected, int desired) {
if (value.compare_exchange_strong(expected, desired)) {
std::cout << "Swap successful! New value: " << value << std::endl;
}
else {
std::cout << "Swap failed. Expected: " << expected << ", actual: " << value << std::endl;
}
}
int main() {
std::thread t1(compareAndSwap, 10, 20);
std::thread t2(compareAndSwap, 10, 30);
t1.join();
t2.join();
return 0;
}
compare_exchange_strong
尝试将value
的值从expected
修改为desired
。如果value
等于expected
,修改成功;否则修改失败。compare_exchange_strong
是强比较交换,失败率较低。compare_exchange_weak
是弱比较交换,适合用于循环中。
6. 注意事项和常见坑
6.1 内存顺序(Memory Order)
std::atomic
默认使用顺序一致性(sequential consistency)的内存模型,即所有线程都能以相同的顺序观察到所有原子操作。可以通过传入 memory_order
来调整内存顺序,常见的有:
memory_order_relaxed
:不保证任何顺序,仅保证原子性。适合不依赖顺序的场景。memory_order_acquire
:保证在获取数据后,之前的所有写操作对当前线程可见。memory_order_release
:保证在释放数据前,当前线程的写操作对其他线程可见。memory_order_acq_rel
:结合了acquire
和release
的效果。memory_order_seq_cst
:默认行为,顺序一致性。
常见坑:不理解内存顺序可能导致错误的线程同步行为。在性能敏感的场合,应根据实际需求选择适合的内存顺序。
6.2 复合操作的竞态条件
虽然 std::atomic
可以避免单个变量的竞态条件,但多个操作的组合仍可能出现竞态条件。例如,以下代码虽然使用了 std::atomic
,但仍然存在问题:
std::atomic<int> counter(0);
void update() {
if (counter.load() == 0) { // 竞态条件:其他线程可能已经改变了 counter 的值
counter.store(1);
}
}
在多线程环境下,load
和 store
之间没有原子性保障,可能导致竞态问题。此类场景应使用 compare_exchange
之类的复合原子操作。
6.3 std::atomic_flag
std::atomic_flag
是比 std::atomic
更加基础的原子类型,只支持 clear()
和 test_and_set()
操作,常用于实现锁或自旋锁机制。
#include <atomic>
#include <thread>
#include <iostream>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void lockFunction() {
while (lock.test_and_set(std::memory_order_acquire)) { // 自旋直到获取锁
// 等待锁
}
// 临界区
std::cout << "Thread " << std::this_thread::get_id() << " acquired lock.\n";
lock.clear(std::memory_order_release); // 释放锁
}
int main() {
std::thread t1(lockFunction);
std::thread t2(lockFunction);
t1.join();
t2.join();
return 0;
}
解释:
test_and_set
:设置标志并返回其之前的值。如果标志已被设置(锁已被其他线程持有),则自旋等待。clear
:清除标志,释放锁。
小结
- 优点:
std::atomic
提供轻量、高效的原子操作,适用于不需要复杂锁机制的场景。 - 注意事项:需理解内存顺序对性能和正确性的影响。对于复杂的多操作组合,仍需谨慎处理竞态条件。
- 常见坑:不当使用
load
和store
导致竞态条件、对内存顺序的误解、复合操作的不原子性。
8.知识补充
除了基本用法和常见场景之外,std::atomic
还有一些更深入的知识和高级应用场景,涉及到更复杂的并发控制、性能优化、内存模型等方面。以下是一些进一步的知识点和应用技巧:
1. std::atomic
的内存模型
1.1 顺序一致性(Sequential Consistency)
std::atomic
默认采用顺序一致性模型,即所有的原子操作会按照某种顺序执行,所有线程可以看到这些操作发生的顺序是完全一致的。这种模型虽然最直观,但性能相对较低,特别是在多核系统中。
std::atomic<int> x(0);
std::atomic<int> y(0);
// 顺序一致性保证所有线程可以一致地看到操作顺序
x.store(1); // store 操作
int r1 = y.load(); // load 操作
1.2 更宽松的内存顺序(Relaxed Memory Order)
可以通过指定内存顺序来优化性能,但会引入更复杂的并发问题。以下是几种常见的内存顺序:
compare_exchange
操作的内存顺序较为复杂,可以为成功和失败情况分别指定不同的内存顺序。例如:
1.3 compare_exchange
中的内存顺序
memory_order_relaxed
:只保证操作是原子的,不保证操作顺序。适合一些不依赖其他内存操作顺序的场景,比如计数器递增。-
std::atomic<int> counter(0);
void increment() {
counter.fetch_add(1, std::memory_order_relaxed); // 仅保证原子性,不保证顺序
} -
memory_order_acquire
:当前操作与之前的操作存在依赖关系,保证后续读取到的是最新值。 -
std::atomic<int> flag(0);
void consumer() {
while (!flag.load(std::memory_order_acquire)) { // 依赖之前的写入操作,保证读取最新值
// 自旋等待
}
// 进入临界区
} -
memory_order_release
:保证之前的操作在此之前完成,适合发布事件或信号的场景。 -
void producer() {
// 临界区逻辑
flag.store(1, std::memory_order_release); // 发布信号
} -
memory_order_acq_rel
:结合了acquire
和release
的语义,适合需要在发布前进行依赖检查的场景。 -
memory_order_consume
:类似于acquire
,但仅依赖于使用的操作,而非所有后续操作。它可以提高性能,但其实现和保证依赖编译器。
std::atomic<int> value(0);
int expected = 0;
value.compare_exchange_strong(expected, 1, std::memory_order_acq_rel, std::memory_order_relaxed);
- 成功时采用
acq_rel
,失败时采用relaxed
。这样在不需要成功时的严格顺序控制时,失败的情况下可以更宽松,提升性能。
2. std::atomic
和 volatile
的区别
许多人常常混淆 std::atomic
和 volatile
。二者在多线程中的作用非常不同:
volatile
只是告诉编译器不要对其进行优化,适用于处理与硬件相关的操作,无法保证线程安全。std::atomic
是多线程同步原语,保证原子性和线程安全,适合多线程编程。- volatile int counter = 0; // 不安全,不能用于多线程
std::atomic<int> atomic_counter = 0; // 线程安全
3. std::atomic
的优化与陷阱
3.1 自旋锁与 CPU 过度占用
在高并发的场景下,使用自旋锁时可能导致 CPU 资源的浪费,尤其是在资源紧张的情况下。可以通过结合自旋锁与 std::this_thread::yield()
来减少 CPU 过度占用。
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void spinlock_acquire() {
while (lock.test_and_set(std::memory_order_acquire)) {
std::this_thread::yield(); // 如果锁被占用,主动放弃 CPU 资源
}
}
void spinlock_release() {
lock.clear(std::memory_order_release);
}
3.2 ABA 问题
std::atomic
的 compare_exchange
操作可能遇到 ABA 问题。即一个线程看到变量从 A 变成 B 再变回 A,误以为变量从未被修改过。这在 std::atomic
的 CAS 操作中可能导致数据竞争。解决 ABA 问题的一种方法是使用指针和版本号结合,或使用更高级的 std::atomic<std::shared_ptr>
等技术。
3.3 多线程的伪共享(False Sharing)
伪共享是指多个线程操作不同的 std::atomic
变量,但这些变量位于同一个缓存行(通常是 64 字节),导致缓存一致性协议频繁地刷新和更新缓存行,降低性能。
优化技巧: 可以通过将变量按 64 字节对齐或使用 alignas
关键字避免伪共享。
struct alignas(64) PaddedAtomic {
std::atomic<int> value;
};
4. std::atomic
和非阻塞数据结构
4.1 原子队列(Atomic Queue)
std::atomic
可以用来实现无锁队列、栈等数据结构,避免传统锁带来的开销。无锁数据结构在高并发下表现尤为出色,减少了上下文切换和锁竞争带来的性能损耗。
一个简单的无锁栈例子:
#include <atomic>
struct Node {
int data;
Node* next;
};
std::atomic<Node*> head(nullptr);
void push(int value) {
Node* newNode = new Node{value, nullptr};
newNode->next = head.load();
while (!head.compare_exchange_weak(newNode->next, newNode)) {
// CAS 失败时重新尝试
}
}
int pop() {
Node* oldHead = head.load();
while (oldHead && !head.compare_exchange_weak(oldHead, oldHead->next)) {
// CAS 失败时重新尝试
}
if (oldHead) {
int value = oldHead->data;
delete oldHead;
return value;
}
return -1; // 空栈
}
4.2 原子共享指针(Atomic Shared Pointers)
C++11 中,std::atomic
还支持 std::shared_ptr
,用于管理对象的引用计数。这种技术特别适合实现并发的数据结构,避免对象的过早销毁或误用。
#include <memory>
#include <atomic>
std::atomic<std::shared_ptr<int>> atomicPtr;
void foo() {
auto localPtr = std::make_shared<int>(42);
atomicPtr.store(localPtr);
}
void bar() {
auto loadedPtr = atomicPtr.load();
if (loadedPtr) {
std::cout << *loadedPtr << std::endl;
}
}
5. std::atomic
和线程安全单例模式
std::atomic
可以用于实现线程安全的单例模式:
#include <atomic>
class Singleton {
public:
static Singleton* getInstance() {
Singleton* temp = instance.load(std::memory_order_acquire);
if (!temp) {
std::lock_guard<std::mutex> lock(mtx);
temp = instance.load(std::memory_order_relaxed);
if (!temp) {
temp = new Singleton();
instance.store(temp, std::memory_order_release);
}
}
return temp;
}
private:
Singleton() = default;
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
总结
std::atomic
是 C++ 中强大的并发工具,适用于避免锁机制带来的性能损耗。- 需要理解内存顺序模型以及如何选择适合的内存顺序以提高性能。
- 在高并发场景中可能遇到 ABA 问题、伪共享等高级问题,需根据具体需求进行优化。
- 可以结合
std::atomic
实现无锁数据结构、线程安全的单例模式和原子引用计数等复杂应用。