C++并发编程(三十二)
前言
前文我们介绍了三种无锁并发栈的实现,管理内存回收的方法分别是:通过 pop 线程减少到 1 时的一次性释放,通过风险指针,通过智能指针的内部计数(并非真无锁)。
本文介绍一种通过内部计数和外部计数实现的无锁并发栈,由 node 节点指针储存内部引用计数 internalCount,初始为 0,由 countedNodePtr 节点计数类封装 node 节点和 externalCount 外部引用计数,初始为 1.
栈持有的 head 节点,就是这种由两层封装的节点计数类对象,发起 pop 操作时,首先每个线程都获取 head 节点,并且将 head 的外部引用计数增加 1。
然后进入我们熟悉的 CAS 比较替换,只有一个线程会成功弹出 head 节点,其余线程则不会占用 head,但每个线程都会减少 1 个 head 的内部引用计数,当内部引用计数加外部引用计数和为0,则删除 head 所占用的节点资源。
一、由内外引用计数实现的无锁并发栈
重点提示: 以下实现不一定适用于所有平台!
在本人的 C++ 工具链 + 硬件平台中,也需要特殊的编译参数,开启可封装16字节大小的原子类,并且确保原子的基本操作无锁。如果你的平台不支持,则以下代码无效,可选用前文介绍的三种实现。
本人的 C++ 工具链:
操作系统:win10 x64
msys2平台:
clang version 15.0.0
Target: x86_64-w64-windows-gnu
Thread model: posix
编辑器:vscode 1.72.0
硬件平台:AMD Ryzen 7 1700 8-Core Processor
编译参数:
clang++.exe -glldb -std=c++17 -lpthread -march=native learnThread_91.cpp -o learnThread_91.exe -Ik:/msys64/clang64/include/
必须添加如下参数,开启多线程,
-lpthread
以及只适用于本机,不可兼容的编译选项,无法将本设备编译的可执行文件用于其它平台。
-march=native
以下是实现代码,和前面的实现一样,代码不复杂,但逻辑很复杂,每一步多需要你模拟多个线程同时乱序的执行如下代码,通过原子操作约束并发逻辑。
#include <array>
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
namespace noLock
{
// 无锁栈类
template <typename T>
struct lockFreeStack;
// 节点类
template <typename T>
struct node;
// 外部节点计数类
template <typename T>
struct countedNodePtr
{
// 默认构造
countedNodePtr() = default;
countedNodePtr(int cnt, node<T> *rhsPtr)
: externalCount(cnt)
, ptr(rhsPtr)
{}
private:
// 外部计数
int64_t externalCount = 0;
// 节点指针
node<T> *ptr = nullptr;
friend struct lockFreeStack<T>;
};
template <typename T>
struct node
{
// 构造节点,封装数据,外部计数置零
explicit node(const T &datas)
: data(std::make_shared<T>(datas))
, internalCount(0)
{}
private:
// 只能指针封装的数据
std::shared_ptr<T> data;
// 原子类内部计数
std::atomic<int> internalCount = 0;
// 外部节点计数类
countedNodePtr<T> next;
friend struct lockFreeStack<T>;
};
template <typename T>
struct lockFreeStack
{
// 默认构造,置空 head 节点
lockFreeStack()
{
head.store(countedNodePtr<T>());
}
// 不可拷贝构造
lockFreeStack(const lockFreeStack &rhs) = delete;
// 不可拷贝赋值
auto operator=(const lockFreeStack &rhs) -> lockFreeStack & = delete;
// 析构
~lockFreeStack()
{
// 获取 head 中外部节点计数类中的节点指针
node<T> *ptr = head.load().ptr;
// next 节点
node<T> *next = nullptr;
// 若节点指针不空,则删除并后移
while (ptr)
{
next = ptr->next.ptr;
delete ptr;
ptr = next;
}
}
// head 是否无锁结构
auto isNoLock() -> bool
{
return head.is_lock_free();
}
// 数据压栈
void push(const T &datas)
{
// 新建节点计数类对象,外部计数为 1,内部计数为 0
countedNodePtr<T> const newNode(1, new node(datas));
// 获取头节点计数类变量
newNode.ptr->next = head.load();
// 更新 newNode.ptr->next 或将 newNode 赋值给 head
while (!head.compare_exchange_weak(newNode.ptr->next, newNode))
{}
}
// 数据弹出栈
auto pop() -> std::shared_ptr<T>
{
// 获取头节点计数类变量
countedNodePtr<T> oldHead = head.load();
// 循环
while (true)
{
// 增加头节点外部计数
increaseHeadCount(oldHead);
// 获取头节点指针
node<T> *const headNodePtr = oldHead.ptr;
// 空指针返回空智能指针
if (!headNodePtr)
{
return std::shared_ptr<T>();
}
// 如果 head 等于 oldHead,更新 head,进入 if 程序
// 否则更新 oldHead 跳出 if
if (head.compare_exchange_strong(oldHead, headNodePtr->next))
{
// 要返回的结果值
std::shared_ptr<T> res;
// 获取结果值
res.swap(headNodePtr->data);
// 获取计数,减二是因为节点弹出,自身外部引用的 1 被减去
// 同时,本线程不再访问此节点,本线程所加的 1 也要减去
const int countIncrease = oldHead.externalCount - 2;
// 如果内部计数值等于负外部计数值,则删除头节点指针,
// 否则内部计数值更新为原值加外部计数值
if (headNodePtr->internalCount.fetch_add(countIncrease) ==
-countIncrease)
{
delete headNodePtr;
}
return res;
}
// 如果内部计数减一等于一,则删除头节点指针
if (headNodePtr->internalCount.fetch_sub(1) == 1)
{
delete headNodePtr;
}
}
}
private:
// 原子封装的外部节点计数类 head
std::atomic<countedNodePtr<T>> head;
// 增加头节点外部计数
void increaseHeadCount(countedNodePtr<T> &oldCounter)
{
countedNodePtr<T> newCounter;
// 循环
do
{
// 获取原计数对象值
newCounter = oldCounter;
// 增加外部计数
++newCounter.externalCount;
// 若头节点计数对象等于 oldCounter,更新头节点,否则更新 oldCounter
} while (!head.compare_exchange_strong(oldCounter, newCounter));
// 更新 oldCounter 计数,用以在 pop 中的后续比较替换
oldCounter.externalCount = newCounter.externalCount;
}
};
} // namespace noLock
auto main() -> int
{
noLock::lockFreeStack<int> lfs;
std::cout << lfs.isNoLock() << std::endl;
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;
}
对于以上实现,我们还可以添加相应的内存次序来增加原子操作性能,提升效率。
这里设想 push 一些数据,然后进行 pop,push 和 pop 中的 increaseHeadCount(oldHead) 形成释放获取的同步次序,保证 head 必然是头节点。
pop 中成功弹出 head 节点后,有 headNodePtr->internalCount.fetch_add() 操作,
其它 pop 线程则是 headNodePtr->internalCount.fetch_sub() 操作,
前面 add 设置为 memory_order_release,后面 sub 设置为memory_order_acquire 构成同步,简单明了,
不过参考文献作者还要更进一步,把 sub 设为宽松次序,因为失败的线程可能很多,只有计数成功置零才通过 load 操作进行 memory_order_acquire 同步一下,如何选择,看自己吧。
#include <array>
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
// 不对执行顺序做任何保证(全部适用)
// std::memory_order_relaxed
//
// 全部存取都按顺序执行(全部适用)
// std::memory_order_seq_cst
//
// 本线程中,所有之前的写操作完成后才能执行本条原子操作(写适用 / 读_改_写适用)
// std::memory_order_release
//
// 本线程中,所有后续的有关本原子类型的操作,必须在本条原子操作完成后执行(读适用,读_改_写适用)
// std::memory_order_consume
//
// 本线程中,所有后续的读操作必须在本条原子操作完成后执行(读适用,读_改_写适用)
// std::memory_order_acquire
//
// 同时包含:
// memory_order_acquire:所有后续的读操作必须在本条原子操作完成后执行和
// memory_order_release:所有之前的写操作完成后才能执行本条原子操作
// (读_改_写适用)
// std::memory_order_acq_rel
namespace noLock
{
// 无锁栈类
template <typename T>
struct lockFreeStack;
// 节点类
template <typename T>
struct node;
// 外部节点计数类
template <typename T>
struct countedNodePtr
{
// 默认构造
countedNodePtr() = default;
countedNodePtr(int cnt, node<T> *rhsPtr)
: externalCount(cnt)
, ptr(rhsPtr)
{}
private:
// 外部计数
int64_t externalCount = 0;
// 节点指针
node<T> *ptr = nullptr;
friend struct lockFreeStack<T>;
};
template <typename T>
struct node
{
// 构造节点,封装数据,外部计数置零
explicit node(const T &datas)
: data(std::make_shared<T>(datas))
, internalCount(0)
{}
private:
// 只能指针封装的数据
std::shared_ptr<T> data;
// 原子类内部计数
std::atomic<int> internalCount = 0;
// 外部节点计数类
countedNodePtr<T> next;
friend struct lockFreeStack<T>;
};
template <typename T>
struct lockFreeStack
{
// 默认构造,置空 head 节点
lockFreeStack()
{
head.store(countedNodePtr<T>());
}
// 不可拷贝构造
lockFreeStack(const lockFreeStack &rhs) = delete;
// 不可拷贝赋值
auto operator=(const lockFreeStack &rhs) -> lockFreeStack & = delete;
// 析构
~lockFreeStack()
{
// 获取 head 中外部节点计数类中的节点指针
node<T> *ptr = head.load().ptr;
// next 节点
node<T> *next = nullptr;
// 若节点指针不空,则删除并后移
while (ptr)
{
next = ptr->next.ptr;
delete ptr;
ptr = next;
}
}
// head 是否无锁结构
auto isNoLock() -> bool
{
return head.is_lock_free();
}
// 数据压栈
void push(const T &datas)
{
// 新建节点计数类对象,外部计数为 1,内部计数为 0
countedNodePtr<T> const newNode(1, new node(datas));
// 获取头节点计数类变量
newNode.ptr->next = head.load(std::memory_order_relaxed);
// 更新 newNode.ptr->next 或将 newNode 赋值给 head
// memory_order_release
// 本线程中,所有之前的写操作完成后才能执行本条原子操作
// (写适用 读_改_写适用)
// memory_order_relaxed
// 不对执行顺序做任何保证(全部适用)
while (!head.compare_exchange_weak(newNode.ptr->next, newNode,
std::memory_order_release,
std::memory_order_relaxed))
{}
}
// 数据弹出栈
auto pop() -> std::shared_ptr<T>
{
// 获取头节点计数类变量
countedNodePtr<T> oldHead = head.load(std::memory_order_relaxed);
// 循环
while (true)
{
// 增加头节点外部计数
increaseHeadCount(oldHead);
// 获取头节点指针
node<T> *const headNodePtr = oldHead.ptr;
// 空指针返回空智能指针
if (!headNodePtr)
{
return std::shared_ptr<T>();
}
// 如果 head 等于 oldHead,更新 head,进入 if 程序
// 否则更新 oldHead 跳出 if
// increaseHeadCount 的内存次序保证同步,
// 并且只有一个线程可进入下一步,
// 此处可用宽松次序
if (head.compare_exchange_strong(oldHead, headNodePtr->next,
std::memory_order_relaxed))
{
// 要返回的结果值
std::shared_ptr<T> res;
// 获取结果值
res.swap(headNodePtr->data);
// 获取计数,减二是因为节点弹出,自身外部引用的 1 被减去
// 同时,本线程不再访问此节点,本线程所加的 1 也要减去
const int countIncrease = oldHead.externalCount - 2;
// 如果内部计数值等于负外部计数值,则删除头节点指针,
// 否则内部计数值更新为原值加外部计数值
if (headNodePtr->internalCount.fetch_add(
countIncrease, std::memory_order_release) ==
-countIncrease)
{
delete headNodePtr;
}
return res;
}
// 如果内部计数减一等于一,则删除头节点指针
if (headNodePtr->internalCount.fetch_sub(
1, std::memory_order_relaxed) == 1)
{
headNodePtr->internalCount.load(std::memory_order_acquire);
delete headNodePtr;
}
}
}
private:
// 原子封装的外部节点计数类 head
std::atomic<countedNodePtr<T>> head;
// 增加头节点外部计数
void increaseHeadCount(countedNodePtr<T> &oldCounter)
{
countedNodePtr<T> newCounter;
// 循环
do
{
// 获取原计数对象值
newCounter = oldCounter;
// 增加外部计数
++newCounter.externalCount;
// 若头节点计数对象等于 oldCounter,更新头节点,否则更新
// oldCounter
// memory_order_acquire
// 本线程中,所有后续的读操作必须在本条原子操作完成后执行(读适用,读_改_写适用)
// 和 push 线程构成释放获取次序完成同步
} while (!head.compare_exchange_strong(oldCounter, newCounter,
std::memory_order_acquire,
std::memory_order_relaxed));
// 更新 oldCounter 计数,用以在 pop 中的后续比较替换
oldCounter.externalCount = newCounter.externalCount;
}
};
} // namespace noLock
auto main() -> int
{
noLock::lockFreeStack<int> lfs;
std::cout << lfs.isNoLock() << std::endl;
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;
}
总结
无锁结构需要原子操作的约束,但除了标准库的默认实现,其它自定义类的原子类封装则具有非常强的平台因素,难以做到二进制兼容,同时难以保证原子操作的无锁。
所以具体的实现和使用,需要长期的测试,并发逻辑本身就不好把握,而无锁并发,则更为底层,更为琐碎,在逻辑正确且平台支持的情况下,一般性能会更优,但仍需要性能测试去验证。
参考文献:C++并发编程实战(第2版)[英] 安东尼•威廉姆斯(Anthony Williams)