一:概述:
当我们在并发程序中使用队列时,通常会使用锁来同步推入(push)和弹出(pop)操作。我们会在执行推入或弹出操作之前加锁,并在函数结束之前解锁。然而,锁的使用会带来一些开销,包括创建和获取锁的成本。因此,如果我们能够完全避免使用锁,并仍然支持并发操作,那将是非常理想的。对于许多数据结构,如链表、栈和队列,我们可以借助原子操作中的比较并交换(CAS)来实现同步、无锁版本。在 C++ 中,它被称为 std::atomic<T>::compare_exchange_weak
。
二:什么是CAS
CAS 最初是 IBM 在 1973 年引入的 CPU 指令。请记住,只有原始的 CPU 指令才能真正做到原子操作。我们要感谢硬件工程师们给我们带来了 CAS,因为它确实是一个非常棒且有用的指令。
std::atomic<T>::compare_exchange_weak
会将 std::atomic<T>
的当前值与预期值进行比较,如果相等,则将 std::atomic<T>
的值替换为期望的目标值。以下是一个简化的 C++ 代码示例,展示了它的工作原理:
template <typename T>
bool compare_exchange_weak_example(std::atomic<T>& atomic_var, T& expected, T desired) {
// 进行比较交换
bool success = atomic_var.compare_exchange_weak(expected, desired);
// 如果交换成功,返回 true;否则,返回 false
return success;
}
三:实现
template<typename T>
class lock_free_queue
{
private:
// 节点结构,队列中的每个元素都封装在一个节点中
struct node
{
std::shared_ptr<T> data; // 存储数据,使用 shared_ptr 自动管理内存
std::atomic<node*> next; // 原子指针,指向下一个节点
node(T const& data_) :
data(std::make_shared<T>(data_)) {} // 构造函数,初始化节点数据
};
std::atomic<node*> head; // 队列头部的原子指针
std::atomic<node*> tail; // 队列尾部的原子指针
public:
// 向队列尾部添加一个元素
void push(T const& data)
{
// 创建一个新的节点并初始化数据
std::atomic<node*> const new_node = new node(data);
// 获取当前尾节点的指针
node* old_tail = tail.load();
// 尝试更新尾节点的 next 指针,直到成功
while (!old_tail->next.compare_exchange_weak(nullptr, new_node)) {
// 如果尾节点的 next 指针不是 nullptr,表示其他线程已经插入了节点
// 需要重新加载尾节点并重试
old_tail = tail.load();
}
// 尝试将尾节点更新为新的节点
tail.compare_exchange_weak(old_tail, new_node);
}
// 从队列头部弹出一个元素
std::shared_ptr<T> pop()
{
// 获取当前头节点的指针
node* old_head = head.load();
// 尝试更新队列的头节点,直到成功
while (old_head &&
!head.compare_exchange_weak(old_head, old_head->next)) {
// 如果头节点的 next 指针不为空,表示其他线程已经移除了节点
// 需要重新加载头节点并重试
old_head = head.load();
}
// 如果成功获取到头节点,则返回数据;否则返回空的 shared_ptr,表示队列为空
return old_head ? old_head->data : std::shared_ptr<T>();
}
};