使用shared_ptr实现懒式并发链表集合时出现段错误问题咨询
你遇到的问题其实很典型——虽然shared_ptr的引用计数增减是原子操作,但这只解决了单个对象的生命周期管理,懒式并发链表的场景下还有额外的并发冲突需要处理,光靠shared_ptr本身的线程安全特性远远不够。下面我拆解下核心原因和解决思路:
为什么会出现double free或段错误?
1. 混用原始指针与shared_ptr
这是最常见的坑:如果你的代码中存在以下情况,必然会导致double free:
- 手动调用
delete释放某个节点,而该节点还被shared_ptr持有; - 用
shared_ptr::get()获取原始指针后,又用这个指针构造了另一个shared_ptr(两个独立的引用计数,最终都会尝试释放同一内存); - 链表的
next指针用了原始指针而非shared_ptr,导致节点的生命周期管理失控。
2. shared_ptr的线程安全边界被误解
shared_ptr的线程安全仅局限于引用计数的原子操作:多个线程可以同时对同一个shared_ptr实例进行拷贝/销毁(增减引用计数),不会有数据竞争。但:
shared_ptr指向的对象(你的链表节点)本身不是线程安全的。多个线程同时修改节点的next指针、删除标记位时,会产生竞态条件;- 普通
shared_ptr的赋值操作不是原子的。如果链表的next是普通shared_ptr,多线程修改next时可能导致指针的部分写入,进而引发段错误。
3. 懒式删除的并发冲突
懒式集合的核心是"标记删除+延迟移除":节点先被标记为已删除,后续操作跳过它,之后再从链表中物理移除。这个过程中会出现两种问题:
- 线程A标记节点为已删除,线程B此时刚好持有该节点的
shared_ptr并尝试访问next指针,而线程C已经把该节点从链表中移除并修改了next,导致B访问到无效内存; - 节点被标记删除后,仍有线程持有它的
shared_ptr,当最后一个shared_ptr析构时释放节点,但此时可能还有线程在尝试访问该节点的成员(比如标记位),引发段错误。
可行的解决方案
1. 彻底用shared_ptr(或atomic_shared_ptr)管理所有节点引用
- 确保链表的
next指针使用std::atomic_shared_ptr<Node>(C++20及以上支持),它提供了原子的加载、存储、交换操作,避免多线程修改next时的竞态; - 绝对不要手动调用
delete,也不要保留任何节点的原始指针(除非是临时用于读取,且确保该节点不会被释放)。
示例节点结构:
#include <memory> #include <atomic> struct Node { int value; std::atomic<bool> is_deleted{false}; // 原子标记删除状态 std::atomic_shared_ptr<Node> next; // 原子化的next指针 Node(int val) : value(val) {} };
2. 为节点操作添加同步机制
懒式链表的并发安全需要结合"节点级锁"或原子操作:
- 对节点的标记删除、
next指针修改等操作,使用节点内部的互斥锁保护(比如每个节点加std::mutex),避免多线程同时修改同一节点的状态; - 遍历链表时,遵循"手锁"模式:拿到当前节点的锁后,再加载下一个节点的
shared_ptr,确认下一个节点未被删除后,再释放当前节点的锁,避免持有锁时间过长影响并发度。
3. 规范懒式删除的流程
正确的懒式删除步骤应该是:
- 查找目标节点,拿到它的
shared_ptr并锁定节点; - 检查节点是否已被删除,如果是则解锁并返回;
- 标记节点为已删除;
- 找到节点的前驱节点,锁定前驱,将前驱的
next指向当前节点的next; - 释放所有锁,此时节点的
shared_ptr引用会逐渐减少,最终由最后一个持有者自动释放。
4. 避免访问已标记删除的节点
在所有操作(查找、插入、删除)中,遍历链表时要随时检查节点的is_deleted标记,一旦发现已删除的节点,立即跳过并尝试将其从链表中移除(需要同步),避免后续线程再持有该节点的引用。
总结
shared_ptr的原子引用计数确实能帮你自动管理节点内存,但它解决不了并发环境下链表结构修改、节点状态读写的竞态问题。你需要结合原子类型、互斥锁,以及规范的懒式删除流程,才能实现真正安全的懒式并发链表集合。
内容的提问来源于stack exchange,提问作者Dee Jay
相关产品推荐
相关产品推荐

