无锁编程是一种高级的并发编程范式,它通过使用原子操作和内存顺序来避免使用传统的互斥锁,从而在某些场景下可以实现更高的性能和可扩展性。
1.核心思想:std::atomic
无锁编程的基石。它保证了对该对象的操作是原子的、不可分割的。这意味着一个线程写入atomic 变量的同时,另一个线程读取它,只会看到写入前或者写入后的完整值,绝不会看到一个半成品(例如:一个只写了一半的结构体)。
基础类型 ( 如int bool pointer )的std::atomic 特化通常会被编译器直接翻译为底层平台的原子指令( 如x86的LOCK 前缀指令),这些特化是无锁的。你可以通过is_always_lock_free() 或者is_lock_free() 成员函数来检查。
std::atomic<int> counter;
if (counter.is_lock_free())
{
std::out<<"This atomic<int> is lock-free"<<std::endl
}2."无锁"的真正含义
“无锁”并不意味着“没有同步”。它的精确定义是:系统中至少有一个线程能够保持继续前进,而不管其他线程的状态如何。
这通常通过原子操作和循环(使用compare_exchange_weak/compare_exchange_strong)来实现。如果一个线程在操作时被挂起(例如:被操作系统调度中断),它不会阻塞其他想要操作相同数据的线程。其他线程可以继续运行他们的循环并完成操作。
这与基于锁的编程形成鲜明对比:如果一个线程持有一把锁然后被挂起,其他视图获取该锁的线程都会被阻塞,整个系统可能因此停滞。
重要区别:
无锁:保证系统整体不会因为某个线程挂起而死锁,但个别线程可能会被“饿死”(一直循环失败);
无等待:一个更强的保证,每个线程都能在有限步内完成操作,不会饿死。实现起来及其复杂。
3.关键工具:compare_exchange (CAS)
这是无锁编程中最重要的操作,全程是“比较并交换”。它是由一条硬件实现的原子指令。
compare_exchange_weak / compare_exchange_strong
工作原理:它会原子地完成以下步骤:
1.比较原子变量的当前值是否与预期值相同
2.如果相同,则将原子变量的值设置为目标值,操作成功,返回true
3.如果不同,则将预期值更新为原子变量的当前值,操作失败,返回false
Weak vs Strong:
weak版本可能在即使值相等的情况下也失败(伪失败,通常发生在某些平台上),所以它必须在循环中。strong版本则不会产生这种伪失败,但可能性能稍差。在大多数情况下,在循环里使用weak是标准做法。
//无锁栈的Push操作
template<typename T>
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(const T& data) : data(data), next(nullptr) {}
};
std::atomic<Node*> head;
public:
void push(const T& data) {
Node* new_node = new Node(data);
new_node->next = head.load(); // 1. 读取当前头节点
// 2. 循环尝试更新 head
// 如果 head 仍然等于我们之前读取的 new_node->next (期望值)
// 则将 head 设置为我们的 new_node (目标值)
while (!head.compare_exchange_weak(new_node->next, new_node)) {
// 如果 CAS 失败,说明有其他线程修改了 head
// compare_exchange_weak 自动将 new_node->next 更新为了最新的 head
// 所以我们现在只需要重试即可
}
}
};4.内存顺序
这是无锁编程中最复杂、最微妙的地方。它规定了原子操作周围的内存访问(对非原子变量)的可见性顺序。std::memory_order 允许你放松默认的顺序一致性 (std::memory_order_seq_cst)约束,以获取更高的性能。
std::memory_order_seq_cst: 最严格的排序。所有线程看到的操作顺序都一致。性能开销最大。是load 和 store 的默认参数
std::memory_order_acquire: 通常用于读操作。保证当前线程中之后的所有读/写操作不会被重排到该Acquire操作之前
std::memory_order_release: 通常用于写操作。保证当前线程中之前的所有读/写操作不会被重排到该relase操作之后
std::memory_order_relaxed: 只保证原子性,不提供任何同步或者顺序保证。非常快,但是极难正确使用。
//Acquire-Release 语义示例
std::atomic<bool> ready{false};
int data = 0;
// Thread 1 (Producer)
data = 42; // 1. 写入一些数据
ready.store(true, std::memory_order_release); // 2. 发布标志。第 1 步保证在第 2 步之后对其他线程可见
// Thread 2 (Consumer)
while (!ready.load(std::memory_order_acquire)) { // 3. 获取标志
// spin...
}
std::cout << data; // 4. 这里一定会看到 42
// 因为 acquire-load 同步了 release-store除非你非常清楚自己在做什么,否则最好从 memory_order_seq_cst 开始,只有在性能分析证明其是瓶颈后,才在极小的、精心验证的代码段中使用更宽松的内存顺序。