2022-10-08 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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值