无锁(lock-free)实现线程安全的栈的关键在于使用 C++ 的原子操作(std::atomic
和 std::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_front
和 pop_front
:修改栈的操作,使用原子操作保证线程安全。
find
和 front
:读取栈的操作,部分线程安全。
concurrent_stack
的线程安全主要依赖以下机制:
-
使用
std::atomic<std::shared_ptr<Node>>
。head
是std::atomic<std::shared_ptr<Node>>
类型,C++20 提供的特化,支持对shared_ptr
的原子操作。原子操作确保多个线程可以安全地读取和修改head
,而不会导致数据竞争。 -
原子操作:
load
和compare_exchange_weak
。head.load()
:原子地读取head
的值。head.compare_exchange_weak()
:原子地比较并交换head
的值,用于修改链表结构。这些操作是无锁(lock-free)的,避免了传统锁的阻塞。 -
智能指针
std::shared_ptr
使用shared_ptr
管理节点内存,确保线程安全地访问和销毁节点。
禁用拷贝构造和拷贝复制:
拷贝构造和拷贝复制,默认会对 head 进行浅拷贝,浅拷贝会导致两个 concurrent_stack 对象共享同一个 head(指向同一链表)。即使不考虑线程安全,拷贝一个并发数据结构在逻辑上也是错误的。进行深拷贝也不合理,深拷贝需要遍历链表并创建新节点,但遍历过程中链表可能被其他线程修改,导致不一致,深拷贝需要复制整个链表,性能开销很大,在并发环境中难以实现,需要锁住整个链表。
构造函数和析构函数:
cpp
concurrent_stack() : head(nullptr) {}
~concurrent_stack() = default;
构造函数本身是单线程的,在对象构造完成前不会引发竞争。初始化 head
为 nullptr
,由于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_front
或 pop_front
),如果 p
指向的节点被 pop_front
移除,p->next
可能指向一个已被销毁的节点,如果新节点被插入到 head
,find
可能错过新插入的节点。
p
是一个 shared_ptr
,只要 p
持有引用,节点就不会被销毁,即使它被从链表中移除。因此,访问 p->next
和 p->t
是安全的(不会导致内存错误)。但是find
遍历时看到的是链表的"快照",可能不反映最新状态,例如,线程 A 在遍历到某个节点时,线程 B 可能在头部插入一个符合条件的节点,线程 A 不会看到它。
return reference(std::move(p));
返回一个 reference
对象,p
的所有权被转移给 reference
。reference
类持有 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;