04. 简化无锁栈内存管理代码

news/2025/4/2 18:04:24/文章来源:https://www.cnblogs.com/lang77/p/18803391

在 03. 无锁栈的内存管理 我们讨论了两种检测是否节点可以被删除的方法:reference count 和 hazard pointers 方法,但是事实上是它们的管理方式比较复杂,需要考虑的比较多,而说到生命周期的管理,我们很自然而然地会想到类似 std::shared_ptr<> 的使用,所以在这里我们尝试使用一些方法简化无数数据结构的内存管理

使用 lock-free std::shared_ptr<> 自动管理

// Listing 7.9 A lock-free stack using lock-free std::shared_ptr
template <typename T>
class lock_free_stack {private:struct node {// data 使用 shared_ptr 是为了异常返回安全// 在 push 的时候就构造出来,然后在 pop() 时就直接返回一个 shared_ptr// 因为返回一个 shared_ptr 是不会抛出异常的std::shared_ptr<T> data;// 使用 shared_ptr 自动管理 stack 的 node 的生命周期// 避免使用裸指针std::shared_ptr<node> next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};// 使用 shared_ptr 自动管理 stack 的 node 的生命周期// 避免使用裸指针std::shared_ptr<node> head;public:// 实现 push 的逻辑和之前的一致,基本的 3 步走// 只是 node 使用 shared_ptr 进行包装void push(T const& data) {std::shared_ptr<node> const new_node = std::make_shared<node>(data);new_node->next = std::atomic_load(&head);while (!std::atomic_compare_exchange_weak(&head, &new_node->next, new_node));}std::shared_ptr<T> pop() {std::shared_ptr<node> old_head = std::atomic_load(&head);while (old_head &&!std::atomic_compare_exchange_weak(&head, &old_head, std::atomic_load(&old_head->next)));if (old_head) {std::atomic_store(&old_head->next, std::shared_ptr<node>());return old_head->data;}return std::shared_ptr<T>();}~lock_free_stack() { while (pop()); }
};

这段代码其实重点就是在 pop() 部分,其实和之前的差不多,就是对应逻辑的 4 步走;但是值得注意的部分是

if (old_head) {std::atomic_store(&old_head->next, std::shared_ptr<node>());return old_head->data;
}

当取出了对应的节点后,要将对应节点的 next 设置为 nullptr,这点怎么理解呢?在原文中有写到

Note the need to clear the next pointer from the popped node in order to avoid the potential for deeply nested destruction of nodes when the last std::shared_ptr referencing a given node is destroyed.

考虑以下场景

A -> B -> C -> ...

A 引用 B,B 运用 C ... 在这个时候,如果 A 被 pop 出来,在析构 A 的时候,会调用对应 shared_ptr<B> 的析构,因为 A 保存着对 B 的唯一引用,所以这个时候连带着 B 也会被析构,而同理,后续的 C 等等也都会被调用析构函数,这样的链式引用可能会造成说调用析构函数次数太多,栈溢出。

好的,我们理解了这种链式引用的问题,但是你还是觉得不对劲,你认为在当前的场景下,如果 A 被 pop 出,A 对应的对 B 的引用会被 -1, 但是按照逻辑,这个时候 head 会指向 B,所以即使 A 析构的也不应该会造成这样链式引用多次析构的问题...

但事实是,因为这是一个多并发的场景,其实你并不能保证 head 对 B 的引用是长期有效的,即使说可能在之后另一个线程就将 B pop 出了,所以在一定程度上还是会有这样的问题潜在存在,因此显式地将 next 设置为 nullptr 切断对应的链式连接是必要的,也可以看做是防御性编程。

使用 std::experimental::atomic_shared_ptr<>

上面的使用 std::shared_ptr<> 在实现上确实变得清晰简洁,但事实上事情并不会那么轻松,因为以上的实现是需要依赖于 shared_ptr 的原子操作是 lock-free 的才能说是实现了一个 lock-free 的 stack,而对于 std::atomic<> 说,如果使用 shared_ptr 作为类型,那么他很有可能会退化成为使用锁来实现。

这是因为 std::atomic<T> 只能用于那些 复制构造和赋值操作是 "trivial" 或者支持原子性的 类型。

在 C++ 里,一个类型如果它的复制操作(copy constructor / copy assignment)是编译器默认生成的、没有做复杂的事情,比如没有分配内存、没有引用计数这种“副作用”,那就叫 trivial copy。
std::shared_ptr<T> 的复制操作不是这样简单的:
每次你复制一个 shared_ptr,它都会 修改引用计数(reference count)来记录有多少指针指向同一个对象。这个行为是必须受保护的(通常需要加锁或原子操作),否则在多线程下就会崩。

shared_ptr<T> 的复制不是平凡的,它需要管理引用计数,所以 直接用在 std::atomic<shared_ptr<T>> 会出错或不安全(很多编译器甚至直接禁止这么写)。

因此,C++ 的 Concurrency TS(技术规范)里引入了一个新类型:
std::experimental::atomic_shared_ptr<T>,它是专门为 shared_ptr 提供 原子操作封装 的工具。你不需要手动调用 atomic_load() 或 atomic_store(),这个类内部已经处理好了引用计数的线程安全问题。如果你的平台支持 Concurrency TS,那你可以使用它来简洁而高效地实现

// Listing 7.10 Stack implementation using std::experimental::atomic_shared_ptr<>
template <typename T>
class lock_free_stack {private:struct node {std::shared_ptr<T> data;std::experimental::atomic_shared_ptr<node> next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};std::experimental::atomic_shared_ptr<node> head;public:void push(T const& data) {std::shared_ptr<node> const new_node = std::make_shared<node>(data);new_node->next = head.load();while (!head.compare_exchange_weak(new_node->next, new_node));}std::shared_ptr<T> pop() {std::shared_ptr<node> old_head = head.load();while (old_head && !head.compare_exchange_weak(old_head, old_head->next.load()));if (old_head) {old_head->next = std::shared_ptr<node>();return old_head->data;}return std::shared_ptr<T>();}~lock_free_stack() { while (pop()); }
};

然后,在文章中还提到了使用 split reference counts 来实现可以参考文中 Listing 7.12 + Listing 7.13 的实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909347.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Avalonia 界面效果 滚动的渐变矩形边框

本文将和大家介绍一个 Avalonia 界面效果,制作一个滚动的渐变矩形边框本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如下图所示,录制的gif中颜色存在一些偏差,动画有些卡顿…

Avalonia 界面效果 三个圆实现模糊界面动效背景

本文将和大家介绍一个 Avalonia 动效界面效果,由三个圆带模糊效果实现的模糊界面动效背景,适合用在各种 AIGC 主题的应用里面本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如…

儿子的画

昨天儿子在幼儿园学习了自制小册子,并在上面画画。 晚上睡觉前,心血来潮想要再展示一下他在学校是怎么弄的,于是又一顿操作起来,动作还算麻利,只是完成之后都已过十点了,非要我们帮他配上文字,我一开始不明就理,以为写个标题就好了..., 但最后终于搞懂他是要我帮忙下一…

团队协作管理:贝尔宾团队角色模型学习

“没有完美的个人,只有完美的团队。” 任何企业的领导者要想使自己的企业能够快速地发展和成长,就必须对团队建设的重要性有正确的认识。团队角色理论 团队角色理论是管理学中用于分析和优化团队协作的重要理论,可以帮助管理者认识人才、选拔人才,组建高效率合作团队。也可…

钉钉 + AI 网关给 DeepSeek 办入职

通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。…

网络工程师修仙指北---STP(Spanning Tree Protocol)

网络工程师修仙指北---STP(Spanning Tree Protocol) Hello哇,欢迎来到《网络工程是修仙指北系列》,今天我们接着上一篇VLAN的内容,继续为大家介绍网络交换二层技术中另一个重要的内容---STP 一口小酒🍸,一首歌📻,阿轩带你修成仙! 上一篇中我们讲到,通过VLAN的技术…

「通义灵码+X」公开课开讲啦!和赛博同桌一起完成开发任务 有奖励

在AI技术重塑未来的今天,阿里云通义灵码团队携手高校开发者,推出「通义灵码+X系列公开课」暨赛博同桌计划,为编程学习注入全新活力!活动将于2025年3月12日至4月30日火热进行,无论你是技术小白还是代码达人,都能在这里找到与AI并肩学习的乐趣,赢取限定好礼!让AI成为你的…

云原生 Kafka 问卷调研启动,你的声音很重要!参与赢精美礼品!

Apache Kafka 作为高吞吐的分布式消息系统,支持实时数据采集、传输、存储及处理,广泛应用于日志收集、监控数据聚合、流式数据处理、在线和离线分析等场景,是大数据生态的核心组件。然而,随着云计算的快速发展,传统 Kafka 架构在云环境中的局限性日益凸显。Apache Kafka 作…

掌握FastAPI与Pydantic的跨字段验证技巧

title: 掌握FastAPI与Pydantic的跨字段验证技巧 date: 2025/04/01 00:32:07 updated: 2025/04/01 00:32:07 author: cmdragon excerpt: FastAPI中的Pydantic跨字段一致性验证用于处理用户注册、表单提交等场景中多个字段的联合验证需求。Pydantic通过验证器装饰器和根验证器实…

使用 Ollama 本地模型与 Spring AI Alibaba 的强强结合,打造下一代 RAG 应用

使用 Ollama 本地模型与 Spring AI Alibaba 的强强结合,打造下一代 RAG 应用作者:牧生 Spring AI Alibaba RAG Example 示例项目源码地址: https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-rag-example RAG 应用架构概述 1.1 核…

【2025】简易实用知网爬虫,过程加代码

知网是中国最大的学术文献数据库,包含了大量期刊、会议论文、学位论文和报纸等学术资源。尽管知网提供了强大的在线搜索功能,但用户有时需要通过程序自动化地获取大量文章信息。这时,我们会使用爬虫技术来帮助完成这项任务。 工具选择和前提条件 本次爬取工作,我们选择了 P…

RabbitMQ进阶--分布式事务案例

本节主要讲述一个案例,是使用rabbitmq实现分布式事务,本章从分布式事务以sping的声明式事务,转而到rabbitMQ的分布式事务,一下环境需要的依赖:<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp<…