当多个线程同时修改同一个普通变量时,可能会产生数据竞争。例如,counter++ 实际上包含读取、加一和写回三个步骤,两个线程可能读取到相同的旧值,导致其中一次更新丢失。
C++11 引入了 std::atomic 原子类型,如需使用,则要 #include <atomic>。
std::atomic<T> 可以保证针对这个对象的单次操作不可分割,其他线程不会观察到操作执行到一半的状态。
#include <atomic>
std::atomic<int> counter{0};
void increment() {
counter.fetch_add(1);
}
常用操作包括:
load():读取当前值。store(value):写入新值。fetch_add(value):原子加法,并返回修改前的值。fetch_sub(value):原子减法,并返回修改前的值。exchange(value):写入新值,并返回旧值。
std::atomic<int> value{0};
value.store(10);
int current = value.load();
int old = value.fetch_add(2);
// current == 10
// old == 10
// value == 12
原子操作默认使用 std::memory_order_seq_cst,它提供最容易理解的全局顺序保证。如果变量只是一个独立计数器,不负责通知其他线程「数据已经准备完成」,可以使用 std::memory_order_relaxed:
counter.fetch_add(1, std::memory_order_relaxed);
memory_order_relaxed 仍然保证计数操作本身是原子的,但不会同步其他普通变量。Count-Min Sketch 等并发计数场景通常只关心计数不会丢失,因此可以使用这种内存序。
注意
std::atomic 只保证单次操作的原子性。下面的 load() 和 store() 分别是原子的,但组合起来并不是一次原子递增:
counter.store(counter.load() + 1);
这种情况应该使用 fetch_add(1)。如果需要同时维护多个变量,或者保护包含多个步骤的操作,应使用 std::mutex。
此外,volatile 不能代替 std::atomic。volatile 不提供线程同步,也不能避免数据竞争。