C++并发编程(三十)
前言
上文我们通过利用 pop 线程计数的方法,实现了无锁的并发栈。
本文我们将用风险指针实现无锁并发栈。
风险指针, 顾名思义,是鉴别要删除节点有无风险的信号指针。
就像一家人看电视,所有人都看西游记,你要转台,得问问其他人同意不同意,如果不同意就先等等,等播完了再换。
一、风险指针构造的无锁栈
我们用一个全局的风险指针链表 hazardPtrArray 存储风险指针,每个 pop 线程都会将自己正在访问的 head 节点赋予自己对应的风险指针hazardPtr,
如果成功出栈,则会将本线程风险指针置零 hazardPtr.store(nullptr),同时出栈的节点与风险指针链表中的值进行比较 outstandingHazardPointersFor(oldHead),
如果存在与出栈节点相同的风险指针,则意味着有其它线程还占用着资源,不可删除,需要挂载到一个预回收链表 listToReclaim,否则直接删除节点。最后将预回收链表中的每个值和风险指针链表比较,删除没有风险指针的节点。
下面的实现中,内存回收策略还是有些低效.
#include <array>
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
namespace noLock
{
template <typename T>
struct lockFreeStack;
template <typename T>
struct node
{
explicit node(const T &dataVal)
: data(std::make_shared<T>(dataVal))
{}
private:
std::shared_ptr<T> data;
node *next = nullptr;
friend struct lockFreeStack<T>;
};
// 最大风险指针数量
const unsigned maxHazardPtrNum = 100;
// 风险指针类,含有线程id,及原子void类指针
struct hazardPointer
{
// 线程id
std::atomic<std::thread::id> id;
// 原子void类型指针
std::atomic<void *> pointer = nullptr;
};
// 风险指针数组
std::array<hazardPointer, maxHazardPtrNum> hazardPtrArray;
// 风险指针持有者类
struct hazardPtrOwner
{
// 构造函数
hazardPtrOwner()
{
// 当 i 小于 最大风险指针数时,循环赋予每个线程不同的风险指针
for (unsigned i = 0; i < maxHazardPtrNum; ++i)
{
// 初始化空线程id
std::thread::id oldId;
// 如果风险指针数组中,第 i 个风险指针的 id 为空,
// 则赋予其当前线程 id
if (hazardPtrArray[i].id.compare_exchange_strong(
oldId, std::this_thread::get_id()))
{
// 用风险指针数组中的元素初始化 hazardPtr,并退出循环
hazardPtr = &hazardPtrArray[i];
break;
}
}
if (hazardPtr == nullptr)
{
throw std::runtime_error("No hazard pointers available");
}
}
// 不可拷贝构造
hazardPtrOwner(const hazardPtrOwner &) = delete;
// 不可拷贝赋值
auto operator=(const hazardPtrOwner &) -> hazardPtrOwner & = delete;
// 析构
~hazardPtrOwner()
{
// 原子置空
hazardPtr->pointer.store(nullptr);
// 原子写入 id 为 0
hazardPtr->id.store(std::thread::id());
}
// 获取风险指针的 pointer( 原子<void*> )
auto getPointer() -> std::atomic<void *> &
{
return hazardPtr->pointer;
}
private:
// 风险指针类指针
hazardPointer *hazardPtr = nullptr;
};
// 取得当前线程的风险指针
auto getHzrdPtrForCrtThrd() -> std::atomic<void *> &
{
// 初始化局部线程静态风险指针持有者类对象
thread_local static hazardPtrOwner hazard;
// 返回风险指针持有的原子 <void*>
return hazard.getPointer();
}
// 检查风险指针数组中是否有和 ptr 相同的指针
auto outstandingHazardPointersFor(void *ptr) -> bool
{
for (unsigned i = 0; i < maxHazardPtrNum; ++i)
{
if (hazardPtrArray[i].pointer.load() == ptr)
{
return true;
}
}
return false;
}
// 删除 ptr 指针指向的资源
template <typename T>
void doDelete(void *ptr)
{
delete static_cast<T *>(ptr);
}
// 待回收数据类
struct dataToReclaim
{
dataToReclaim() = delete;
// 构造函数
template <typename T>
explicit dataToReclaim(T *ptr)
: data(ptr)
, deleter(&doDelete<T>)
{}
// 析构函数
~dataToReclaim()
{
deleter(data);
}
private:
// 待回收数据指针
void *data = nullptr;
// 销毁函数
std::function<void(void *)> deleter;
// 待回收数据类指针
dataToReclaim *next = nullptr;
// 友元函数,添加至带回收链表
friend void addToReclaimList(dataToReclaim *node);
// 友元函数,在无风险指针时删除节点
friend void deleteNodesWithNoHazards();
};
// 全局变量,待回收类原子指针链表
std::atomic<dataToReclaim *> listToReclaim;
// 将 node 添加至待回收链表
void addToReclaimList(dataToReclaim *node)
{
// node->next 指向待回收链表头节点
node->next = listToReclaim.load();
// 如果待回收链表头节点等于 node->next 节点,则更新待回收链表头节点为 node
// 否则更新 node->next 循环检查
while (!listToReclaim.compare_exchange_weak(node->next, node))
{}
}
// 由数据指针初始化待回收类对象,并添加至待回收链表
template <typename T>
void reclaimLater(T *data)
{
addToReclaimList(new dataToReclaim(data));
}
// 删除没有风险指针的节点
void deleteNodesWithNoHazards()
{
// 原子化的赋值,将带回收类原子指针链表值赋值给带回收数据类指针 current
dataToReclaim *current = listToReclaim.exchange(nullptr);
// 如果 current 不为空
while (current != nullptr)
{
// 获取 current 的下一节点
dataToReclaim *const next = current->next;
// 如果 current 中带回收数据指针没有在风险指针数组中
if (!outstandingHazardPointersFor(current->data))
{
// 删除 current 节点
delete current;
}
// 否则
else
{
// 将 current 添加至带回收链表
addToReclaimList(current);
}
// 将 current 后移一位
current = next;
}
}
template <typename T>
struct lockFreeStack
{
lockFreeStack() = default;
// 不可拷贝构造
lockFreeStack(const lockFreeStack &rhs) = delete;
// 不可拷贝赋值
auto operator=(const lockFreeStack &rhs) -> lockFreeStack & = delete;
// 析构
~lockFreeStack()
{
deleteNodes(head.load());
}
// 入栈
void push(const T &data)
{
node<T> *newNode = new node(data);
newNode->next = head.load();
// 当前值与期望值相等时,修改当前值为设定值,返回true
// 当前值与期望值不等时,将期望值修改为当前值,返回false
// 这个函数可能在满足true的情况下仍然返回false,所以只能在循环里使用
while (!head.compare_exchange_weak(newNode->next, newNode))
{}
}
// 出栈
auto pop() -> std::shared_ptr<T>
{
// 获取当前线程的风险指针并赋值给 hazardPtr
std::atomic<void *> &hazardPtr = getHzrdPtrForCrtThrd();
// 获取头节点指针并赋值给 oldHead
node<T> *oldHead = head.load();
// 循环,当 oldHead 不为空,且 oldHead 与 head 不等时循环,
// 当 oldHead 不为空,且 oldHead 与 head 相等,更新 head,退出循环
do
{
node<T> *temp;
// 循环,以保证循环结束时,风险指针存入了 head 节点
do
{
temp = oldHead;
hazardPtr.store(oldHead);
oldHead = head.load();
} while (oldHead != temp);
} while (oldHead &&
!head.compare_exchange_strong(oldHead, oldHead->next));
// 当 head 节点更新完,置空风险指针
hazardPtr.store(nullptr);
std::shared_ptr<T> res;
// 处理 oldHead
if (oldHead)
{
// 交换节点值用以返回数据
res.swap(oldHead->data);
// 如果 oldHead 有与风险指针相同值,则将其添加至待回收链表
if (outstandingHazardPointersFor(oldHead))
{
reclaimLater(oldHead);
}
// 否则直接删除
else
{
delete oldHead;
}
// 删除待回收链表中,与风险指针不等的节点
deleteNodesWithNoHazards();
}
return res;
}
private:
// 头节点
std::atomic<node<T> *> head = nullptr;
// 删除节点指向的链表
static void deleteNodes(node<T> *nodes)
{
node<T> *next = nullptr;
while (nodes)
{
next = nodes->next;
delete nodes;
nodes = next;
}
}
};
} // namespace noLock
auto main() -> int
{
noLock::lockFreeStack<int> lfs;
std::thread t0([&lfs] {
for (int i = 0; i != 10000; ++i)
{
lfs.push(i);
}
});
std::thread t1([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
std::thread t2([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
std::thread t3([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
std::thread t4([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
std::thread t5([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
std::thread t6([&lfs] {
for (int i = 0; i != 500; ++i)
{
std::cout << *lfs.pop() << "\n";
}
});
t0.join();
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
return 0;
}
总结
风险指针,理解起来不难,实现起来不容易,并且,这是一个有专利的算法,商业使用有点问题。