1. 为什么需要原子变量?
想象一下你和室友共用一个冰箱,当你们同时打开冰箱门拿牛奶时,可能会发生"牛奶争夺战"。在多线程编程中,std::atomic就是那个帮你避免资源争夺的智能管家。它能让多个线程安全地访问共享数据,而不会出现数据错乱的情况。
传统方式中,我们会用互斥锁(mutex)来保护共享数据,就像给冰箱加把锁。但锁的开销很大,每次拿牛奶都要开锁关锁。而原子操作就像冰箱的智能感应门,不需要显式加锁就能安全存取物品。
我在一个高频交易系统中就遇到过这个问题。最初使用mutex保护计数器,性能测试时发现锁竞争导致吞吐量下降40%。改用atomic后,不仅解决了线程安全问题,性能还提升了35%。
2. 原子变量基础操作
2.1 创建原子变量
创建atomic变量就像声明普通变量一样简单,只是多了一层"原子包装":
#include <atomic> std::atomic<int> counter(0); // 原子整型,初始值为0 std::atomic<bool> ready(false); // 原子布尔值 std::atomic<double> price(99.99); // 原子浮点数(注意有坑!)这里有个实际项目中的经验:不是所有类型都适合做原子操作。比如浮点数的atomic在某些平台可能不是无锁的。我曾经在一个跨平台项目中踩过这个坑,后来改用整型才解决问题。
2.2 基本读写操作
原子变量的读写有专门的方法:
// 写入值 counter.store(42); // 相当于 counter = 42,但是线程安全的 // 读取值 int current = counter.load(); // 相当于 int current = counter // 复合操作 int old = counter.exchange(100); // 把值设为100,返回旧值在开发消息队列时,我发现exchange特别有用。可以用它来实现一个简单的自旋锁:
std::atomic<bool> lock(false); // 获取锁 while(lock.exchange(true)) {} // 忙等待 // 临界区操作... // 释放锁 lock.store(false);3. 原子操作进阶技巧
3.1 原子算术运算
atomic提供了一系列原子算术运算,这些是真正的"杀手锏":
std::atomic<int> value(10); // 原子加法 int prev = value.fetch_add(5); // value +=5,返回旧值 // 原子减法 prev = value.fetch_sub(3); // value -=3 // 原子递增/递减 value++; // 等价于fetch_add(1)在实现无锁队列时,这些操作特别关键。我曾经用fetch_add实现了一个多线程安全的环形缓冲区索引:
std::atomic<size_t> write_index(0); // 多线程安全获取写入位置 size_t get_write_position() { return write_index.fetch_add(1) % buffer_size; }3.2 比较交换(CAS)操作
compare_exchange是原子操作中最强大的武器,它是很多无锁算法的基础:
std::atomic<int> value(100); int expected = 100; bool success = value.compare_exchange_strong(expected, 200); // 如果value==100,就设为200,返回true // 否则把expected设为当前值,返回false我在实现线程安全的LRU缓存时,就大量使用了CAS操作。比如更新缓存命中计数:
void update_hit_count() { int old = hit_count.load(); int new_val; do { new_val = old + 1; } while(!hit_count.compare_exchange_weak(old, new_val)); }这里用了compare_exchange_weak配合循环,比直接用compare_exchange_strong性能更好,特别是在高竞争场景下。
4. 性能优化与陷阱规避
4.1 内存顺序选择
atomic操作可以指定内存顺序,这对性能影响很大:
// 最宽松的顺序,性能最好但同步保证最少 counter.fetch_add(1, std::memory_order_relaxed); // 常用的是acquire-release语义 data.store(new_value, std::memory_order_release); int value = data.load(std::memory_order_acquire);在开发一个高性能网络服务器时,经过测试发现:
- 使用memory_order_seq_cst(默认)时,QPS是12万
- 优化为合适的memory_order后,QPS提升到18万
4.2 常见陷阱
- 误以为所有操作都是原子的
std::atomic<int> x(1); x = x * 2; // 不是原子操作!正确做法是使用fetch_mul(如果支持)或CAS。
ABA问题在CAS操作中,值从A变B又变回A,可能导致错误判断。解决方案是使用带版本号的指针。
虚假失败compare_exchange_weak可能在硬件层面失败,即使值符合预期。所以要用循环重试。
我在实现无锁栈时就遇到过ABA问题,后来改用带计数器的指针才解决:
struct Node { void* data; Node* next; }; struct CountedPtr { Node* ptr; uintptr_t count; }; std::atomic<CountedPtr> top;5. 实际应用案例
5.1 高性能计数器
这是atomic最直接的用途:
class ThreadSafeCounter { public: void increment() { count.fetch_add(1); } int get() const { return count.load(); } private: std::atomic<int> count{0}; };在日志系统中,我们用这种计数器统计各等级的日志数量,性能比mutex版本高3倍。
5.2 无锁队列
更复杂的例子是MPSC(多生产者单消费者)队列:
template<typename T> class LockFreeQueue { public: void push(const T& value) { Node* new_node = new Node{value}; Node* old_tail = tail.load(); while(!tail.compare_exchange_weak(old_tail, new_node)) {} old_tail->next = new_node; } bool pop(T& value) { Node* old_head = head.load(); if(old_head == tail.load()) return false; value = old_head->next->data; head.store(old_head->next); delete old_head; return true; } private: struct Node { T data; Node* next; }; std::atomic<Node*> head, tail; };5.3 双重检查锁定
单例模式的经典实现:
class Singleton { public: static Singleton* getInstance() { Singleton* tmp = instance.load(std::memory_order_acquire); if(!tmp) { std::lock_guard<std::mutex> lock(mutex); tmp = instance.load(std::memory_order_relaxed); if(!tmp) { tmp = new Singleton(); instance.store(tmp, std::memory_order_release); } } return tmp; } private: static std::atomic<Singleton*> instance; static std::mutex mutex; };在框架开发中,这种模式可以避免不必要的锁竞争,提升性能。