C++ 实现无锁线程安全栈

无锁(lock-free)实现线程安全的栈的关键在于使用 C++ 的原子操作(std::atomicstd::atomic_compare_exchange_weak),避免了传统锁(如 std::mutex)带来的性能开销。

代码:

c++ 复制代码
#include <algorithm>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>

using namespace std;

template<typename T>
class concurrent_stack {
    struct Node {
        T t;
        std::shared_ptr<Node> next;
    };

    atomic<shared_ptr<Node> > head;

public:
    concurrent_stack() : head(nullptr) {
    } // 显式初始化 head
    ~concurrent_stack() = default;

    concurrent_stack(const concurrent_stack &) = delete; // 禁用拷贝构造函数

    void operator=(const concurrent_stack &) = delete; // 禁用拷贝赋值运算符

    class reference {
        shared_ptr<Node> p;

    public:
        reference(shared_ptr<Node> p_) : p(p_) {
        }

        T &operator*() { return p->t; }
        T *operator->() { return &p->t; }
    };

    auto find(T t) const {
        auto p = head.load();
        while (p && p->t != t)
            p = p->next;
        return reference(move(p));
    }

    auto front() const {
        return reference(head);
    }

    void push_front(T t) {
        auto p = make_shared<Node>();
        p->t = t;
        p->next = head;
        while (!head.compare_exchange_weak(p->next, p)) {
        }
    }

    void pop_front() {
        auto p = head.load();
        while (p && !head.compare_exchange_weak(p, p->next)) {
        }
    }
};

int main() {
    concurrent_stack<int> stack;

    // 多线程压入元素
    thread t1([&stack]() { stack.push_front(1); });
    thread t2([&stack]() { stack.push_front(2); });
    t1.join();
    t2.join();

    // 获取栈顶元素
    auto front = stack.front();
    if (*front) {
        std::cout << "stack top: " << *front << std::endl; // 可能输出 1 或 2
    }

    // 查找元素
    auto found = stack.find(1);
    if (*found) {
        std::cout << "find: " << *found << std::endl;
    }

    // 弹出元素
    stack.pop_front();
    stack.pop_front();

    return 0;
}

concurrent_stack 是一个基于单向链表的栈,支持 push_front(入栈)、pop_front(出栈)、find(查找)和 front(获取栈顶)操作。

Node:链表节点,包含数据 t(类型为 T)和指向下一个节点的指针 next

head:链表的头指针,类型为 std::atomic<std::shared_ptr<Node>>,支持原子操作。

push_frontpop_front:修改栈的操作,使用原子操作保证线程安全。

findfront:读取栈的操作,部分线程安全。

concurrent_stack 的线程安全主要依赖以下机制:

  1. 使用 std::atomic<std::shared_ptr<Node>>headstd::atomic<std::shared_ptr<Node>> 类型,C++20 提供的特化,支持对 shared_ptr 的原子操作。原子操作确保多个线程可以安全地读取和修改 head,而不会导致数据竞争。

  2. 原子操作:loadcompare_exchange_weakhead.load():原子地读取 head 的值。head.compare_exchange_weak():原子地比较并交换 head 的值,用于修改链表结构。这些操作是无锁(lock-free)的,避免了传统锁的阻塞。

  3. 智能指针 std::shared_ptr使用 shared_ptr 管理节点内存,确保线程安全地访问和销毁节点。

禁用拷贝构造和拷贝复制:

拷贝构造和拷贝复制,默认会对 head 进行浅拷贝,浅拷贝会导致两个 concurrent_stack 对象共享同一个 head(指向同一链表)。即使不考虑线程安全,拷贝一个并发数据结构在逻辑上也是错误的。进行深拷贝也不合理,深拷贝需要遍历链表并创建新节点,但遍历过程中链表可能被其他线程修改,导致不一致,深拷贝需要复制整个链表,性能开销很大,在并发环境中难以实现,需要锁住整个链表。

构造函数和析构函数:

cpp 复制代码
concurrent_stack() : head(nullptr) {}
~concurrent_stack() = default;

构造函数本身是单线程的,在对象构造完成前不会引发竞争。初始化 headnullptr,由于std::atomic<std::shared_ptr<Node>> 的初始化是线程安全的,不会引发数据竞争。

析构函数依赖 shared_ptr 自动释放链表中的节点。析构时,如果有其他线程正在访问 head 或持有 reference 对象,shared_ptr 确保节点不会被提前销毁。但是,如果析构时其他线程仍在访问栈(例如调用 push_front),可能导致未定义行为。为了安全,析构前应确保没有线程正在操作栈(例如通过外部同步机制)。

push_front 方法:

cpp 复制代码
void push_front(T t) {
    auto p = make_shared<Node>();
    p->t = t;
    p->next = head;
    while (!head.compare_exchange_weak(p->next, p)) {}
}

p->next = head; 将新节点的 next 指针指向当前 head。这里 head 是通过 std::atomic<std::shared_ptr<Node>> 访问的,head 的读取是隐式的,但在 C++20 中,p->next = head 会调用 head.load()(原子加载)。这一步读取了 head 的快照,可能是其他线程修改前的值。

while (!head.compare_exchange_weak(p->next, p)) {} 使用原子操作更新 head。比较 head 是否等于 p->next(即 head 是否仍是我们刚加载的值)。如果相等,将 head 更新为 p(新节点),并返回 true,表示成功。如果不相等(说明其他线程修改了 head),更新 p->next 为当前 head 的值,并返回 false,循环重试。compare_exchange_weak 是原子的,确保 head 的更新不会被其他线程干扰。如果多个线程同时调用 push_front,它们会竞争更新 head,但只有一个线程会成功,其他线程会重试。最终,所有线程的元素都会被正确插入,链表结构不会损坏。

不使用锁(std::mutex),而是通过原子操作实现线程安全,避免了锁的阻塞,提高了并发性能,但是在高竞争场景(多个线程同时压入),compare_exchange_weak 可能失败多次,导致重试开销。

pop_front 方法:

cpp 复制代码
void pop_front() {
    auto p = head.load();
    while (p && !head.compare_exchange_weak(p, p->next)) {}
}

while (p && !head.compare_exchange_weak(p, p->next)) {} 使用原子操作移除头节点。compare_exchange_weak 比较 head 是否等于 p(即 head 是否仍是我们刚加载的值),如果相等,将 head 更新为 p->next(移除头节点),并返回 true。如果不相等(其他线程修改了 head),更新 p 为当前 head 的值,并返回 false,循环重试。compare_exchange_weak 确保 head 的更新是原子的。如果多个线程同时调用 pop_front,只有一个线程会成功移除头节点,其他线程会重试。最终,链表结构保持正确,节点不会被错误移除。

移除的节点(旧的 head)由 shared_ptr 管理,当没有引用时(例如 p 超出作用域),节点会自动销毁。即使其他线程持有 reference 对象(指向被移除的节点),shared_ptr 确保节点不会被提前销毁。

find 方法:

cpp 复制代码
auto find(T t) const {
    auto p = head.load();
    while (p && p->t != t)
        p = p->next;
    return reference(std::move(p));
}

遍历过程中,其他线程可能修改链表(例如通过 push_frontpop_front),如果 p 指向的节点被 pop_front 移除,p->next 可能指向一个已被销毁的节点,如果新节点被插入到 headfind 可能错过新插入的节点。

p 是一个 shared_ptr,只要 p 持有引用,节点就不会被销毁,即使它被从链表中移除。因此,访问 p->nextp->t 是安全的(不会导致内存错误)。但是find 遍历时看到的是链表的"快照",可能不反映最新状态,例如,线程 A 在遍历到某个节点时,线程 B 可能在头部插入一个符合条件的节点,线程 A 不会看到它。

return reference(std::move(p)); 返回一个 reference 对象,p 的所有权被转移给 referencereference 类持有 shared_ptr<Node>,确保节点在被使用期间不会销毁。

遍历过程(p->next)不是线程安全的,可能看到不一致的链表状态。如果需要强一致性,可以添加锁(例如 std::mutex),但会降低性能。

front 方法

cpp 复制代码
auto front() const {
    return reference(head);
}

返回的 reference 可能在其他线程修改链表后变得"过时",得益于 shared_ptr,旧节点不会被销毁,但数据可能不再是栈顶元素。所以同样存在一致性问题。

reference 类的线程安全

cpp 复制代码
class reference {
    std::shared_ptr<Node> p;
public:
    reference(std::shared_ptr<Node> p_) : p(p_) {}
    T& operator* () { return p->t; }
    T* operator->() { return &p->t; }
};

reference 持有的节点可能已被从链表中移除(例如通过 pop_front)。 shared_ptr 确保节点不会销毁,用户访问的是节点的"快照"。同样存在数据一致性问题。

存在的问题:

compare_exchange_weak 存在 ABA 问题:线程 A 加载 head(节点 X)。线程 B 弹出 X,然后压入一个新节点 Y,Y 恰好复用了 X 的内存地址。线程 A 执行 compare_exchange_weak,地址匹配,误以为 head 未变,实际链表已改变。shared_ptr 部分缓解了 ABA 问题(因为引用计数防止内存被提前释放)。但逻辑上的 ABA 问题仍可能发生(比如超出shared_ptr 作用域或者被赋值nullptr)。

可以使用带标记的指针(tagged pointer)或版本计数器,记录 head 的版本号,避免 ABA 问题:

cpp 复制代码
struct Head {
    std::shared_ptr<Node> ptr;
    uint64_t version;
};
std::atomic<Head> head;
相关推荐
XiaoyaoCarter1 小时前
每日两道leetcode
c++·算法·leetcode·职场和发展·贪心算法
LIU_Skill1 小时前
SystemV-消息队列与责任链模式
linux·数据结构·c++·责任链模式
矛取矛求1 小时前
STL C++详解——priority_queue的使用和模拟实现 堆的使用
开发语言·c++
Non importa1 小时前
【C++】新手入门指南(下)
java·开发语言·c++·算法·学习方法
pp-周子晗(努力赶上课程进度版)2 小时前
【C++】特殊类的设计、单例模式以及Cpp类型转换
开发语言·c++
海码0073 小时前
【Hot100】 73. 矩阵置零
c++·线性代数·算法·矩阵·hot100
菜鸟学编程o3 小时前
C++:继承
开发语言·c++
努力努力再努力wz3 小时前
【c++深入系列】:万字string详解(附有sso优化版本的string模拟实现源码)
java·运维·c语言·开发语言·c++
hy____1233 小时前
类与对象(上)
开发语言·c++·算法
庐阳寒月3 小时前
动态规划算法:完全背包类问题
数据结构·c++·算法·动态规划