一、原子操作与内存序:多线程同步的基石
在多线程编程中,最让人头疼的问题莫过于数据竞争。想象一下,两个线程同时修改同一个变量,结果会怎样?大概率会出现不可预测的行为。这时候,原子操作就像救世主一样出现了。
C++11 引入了 <atomic> 头文件,提供了一系列原子类型和操作。原子操作的核心特点是:不可分割,即操作要么完全执行,要么完全不执行,不会出现中间状态。
1.1 基本原子操作示例
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter(0); // 原子整型变量
void increment() {
for (int i = 0; i < 1000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
}
}
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的操作是原子的。fetch_add是原子加法操作,std::memory_order_relaxed表示最宽松的内存序(稍后详解)。
1.2 内存序:控制指令执行顺序
原子操作不仅仅是“原子性”,还涉及内存可见性和指令重排。C++ 提供了 6 种内存序:
memory_order_relaxed:只保证原子性,不保证顺序。memory_order_consume:依赖关系的数据同步。memory_order_acquire:确保当前操作之后的读操作不会被重排到前面。memory_order_release:确保当前操作之前的写操作不会被重排到后面。memory_order_acq_rel:acquire+release。memory_order_seq_cst:最严格的顺序一致性(默认)。
std::atomic<bool> flag(false);
int data = 0;
void producer() {
data = 42; // 普通写操作
flag.store(true, std::memory_order_release); // 确保 data 的写入在 flag 之前
}
void consumer() {
while (!flag.load(std::memory_order_acquire)); // 确保 data 的读取在 flag 之后
std::cout << "Data: " << data << std::endl; // 正确读取 42
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
适用场景:
- 计数器、标志位等简单同步场景可用
relaxed。 - 生产者-消费者模型适合
release-acquire配对。 - 复杂同步逻辑可能需要
seq_cst,但性能较低。
二、无锁队列:高性能并发数据结构
锁(如 mutex)虽然能解决同步问题,但会带来性能瓶颈。无锁(lock-free)数据结构通过原子操作实现线程安全,避免线程阻塞。
2.1 单生产者单消费者无锁队列
#include <atomic>
template<typename T>
class LockFreeQueue {
private:
struct Node {
T data;
Node* next;
Node(T val) : data(val), next(nullptr) {}
};
std::atomic<Node*> head;
std::atomic<Node*> tail;
public:
LockFreeQueue() : head(new Node(T())), tail(head.load()) {}
void enqueue(T val) {
Node* newNode = new Node(val);
Node* oldTail = tail.load(std::memory_order_relaxed);
while (!tail.compare_exchange_weak(oldTail, newNode,
std::memory_order_release, std::memory_order_relaxed)) {
// CAS 失败,重试
}
oldTail->next = newNode;
}
bool dequeue(T& result) {
Node* oldHead = head.load(std::memory_order_relaxed);
Node* nextNode = oldHead->next;
if (nextNode == nullptr) return false; // 队列为空
result = nextNode->data;
head.store(nextNode, std::memory_order_relaxed);
delete oldHead;
return true;
}
};
注释说明:
compare_exchange_weak是 CAS(Compare-And-Swap)操作,核心无锁技术。- 此实现仅适用于单生产者单消费者场景。
2.2 多生产者多消费者无锁队列
更复杂的实现需要处理多线程竞争,通常结合 atomic 和内存序优化。
适用场景:
- 高吞吐量任务队列(如日志系统)。
- 实时数据处理(如金融交易系统)。
缺点:
- 实现复杂,容易引入 bug。
- 内存回收问题(如 ABA 问题)。
三、线程局部存储(TLS):线程私有数据优化
某些数据只需要在线程内部使用(如随机数种子、缓存),使用全局变量会导致竞争,而 thread_local 可以解决这个问题。
3.1 thread_local 基本用法
#include <thread>
#include <iostream>
thread_local int threadId = 0; // 每个线程独立副本
void printId() {
std::cout << "Thread ID: " << threadId << std::endl;
}
int main() {
threadId = 1;
std::thread t([]{
threadId = 2;
printId(); // 输出 2
});
printId(); // 输出 1
t.join();
return 0;
}
适用场景:
- 线程局部缓存(如数据库连接池)。
- 避免锁竞争的线程私有数据。
四、总结与实战建议
- 原子操作:优先用于简单同步,注意内存序选择。
- 无锁队列:适合高性能场景,但实现难度高。
- 线程局部存储:优化线程私有数据访问。
注意事项:
- 无锁编程容易引入隐蔽 bug,务必充分测试。
- 内存序错误可能导致难以调试的问题。
- 性能优化前先做 profiling,避免过早优化。
评论