2022-10-06 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;
}


总结

风险指针,理解起来不难,实现起来不容易,并且,这是一个有专利的算法,商业使用有点问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

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

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

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

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

打赏作者

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

抵扣说明:

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

余额充值